From fd553a5f6ba1e9cea7014c202d119de954fd890d Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Sun, 17 Nov 2024 22:38:21 +0100 Subject: [PATCH 01/33] navigation sdk integration (android only) --- .gitignore | 2 + android/build.gradle | 2 + .../gradle/wrapper/gradle-wrapper.properties | 4 +- android/src/main/AndroidManifest.xml | 3 +- .../maps/mapbox_maps/MapboxMapController.kt | 29 +- .../maps/mapbox_maps/MapboxMapsPlugin.kt | 8 + .../maps/mapbox_maps/NavigationController.kt | 344 ++++++++ .../mapbox_maps/mapping/NavigationMappings.kt | 74 ++ .../mapbox_maps/pigeons/NavigationMessager.kt | 631 +++++++++++++++ .../app/src/profile/AndroidManifest.xml | 3 +- example/android/gradle.properties | 3 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 +- example/android/gradlew | 307 +++++--- example/android/settings.gradle | 2 +- example/assets/puck_icon.png | Bin 0 -> 3285 bytes example/lib/main.dart | 2 + example/lib/navigator_example.dart | 190 +++++ example/lib/utils.dart | 12 + lib/mapbox_maps_flutter.dart | 3 + lib/src/annotation/annotation_manager.dart | 3 +- lib/src/callbacks.dart | 8 + lib/src/location_settings.dart | 2 +- lib/src/map_widget.dart | 92 ++- lib/src/mapbox_map.dart | 122 ++- lib/src/mapbox_maps_platform.dart | 16 + lib/src/pigeons/map_interfaces.dart | 37 + lib/src/pigeons/navigation.dart | 736 ++++++++++++++++++ lib/src/style/directions_criteria.dart | 7 + lib/src/style/navigation_styles.dart | 36 + 30 files changed, 2523 insertions(+), 160 deletions(-) create mode 100644 android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt create mode 100644 android/src/main/kotlin/com/mapbox/maps/mapbox_maps/mapping/NavigationMappings.kt create mode 100644 android/src/main/kotlin/com/mapbox/maps/mapbox_maps/pigeons/NavigationMessager.kt create mode 100644 example/assets/puck_icon.png create mode 100644 example/lib/navigator_example.dart create mode 100644 lib/src/pigeons/navigation.dart create mode 100644 lib/src/style/directions_criteria.dart create mode 100644 lib/src/style/navigation_styles.dart diff --git a/.gitignore b/.gitignore index 5ce292510..fee93470e 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,5 @@ app.*.symbols !/dev/ci/**/Gemfile.lock !**/Podfile.lock !**/example/pubspec.lock +android/gradlew +android/gradle/wrapper/gradle-wrapper.jar diff --git a/android/build.gradle b/android/build.gradle index 4235537aa..646a9b0ec 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -79,6 +79,8 @@ if (file("$rootDir/gradle/ktlint.gradle").exists() && file("$rootDir/gradle/lint dependencies { implementation "com.mapbox.maps:android:11.8.0" + implementation "com.mapbox.navigationcore:navigation:3.5.0-beta.1" + implementation "com.mapbox.navigationcore:ui-components:3.3.0" implementation "androidx.annotation:annotation:1.5.0" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" 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..a2f47b605 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,2 @@ - + 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..dc3cf5309 --- /dev/null +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt @@ -0,0 +1,344 @@ +package com.mapbox.maps.mapbox_maps + +import kotlinx.coroutines.launch +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.coroutineScope +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.ui.maps.NavigationStyles +import com.mapbox.navigation.ui.maps.camera.state.NavigationCameraState +import io.flutter.plugin.common.BinaryMessenger + +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) + // 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 { + var firstLocationUpdateReceived = false + + 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()) {} + + // if this is the first location update the activity has received, + // it's best to immediately move the camera to the current user location + if (!firstLocationUpdateReceived) { + firstLocationUpdateReceived = true + navigationCamera.requestNavigationCameraToOverview( + stateTransitionOptions = NavigationCameraTransitionOptions.Builder() + .maxDuration(0) // instant transition + .build() + ) + } + } + } + + 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 + ) + } + } + // 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(waypoints: List, callback: (Result) -> Unit) { + mapboxNavigation?.requestRoutes( + RouteOptions.builder() + .applyDefaultNavigationOptions() + .alternatives(false) + .coordinatesList(waypoints) + .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(null)) + return + } + + callback.invoke(Result.success(point.toFLT())) + } +} \ 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..48550a662 --- /dev/null +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/mapping/NavigationMappings.kt @@ -0,0 +1,74 @@ +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 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( + 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..7eda8f255 --- /dev/null +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/pigeons/NavigationMessager.kt @@ -0,0 +1,631 @@ +// 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 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 bannerInstructionsJson = pigeonVar_list[0] as String? + val voiceInstructionsJson = pigeonVar_list[1] as String? + val currentState = pigeonVar_list[2] as RouteProgressState? + val inTunnel = pigeonVar_list[3] as Boolean? + val distanceRemaining = pigeonVar_list[4] as Double? + val distanceTraveled = pigeonVar_list[5] as Double? + val durationRemaining = pigeonVar_list[6] as Double? + val fractionTraveled = pigeonVar_list[7] as Double? + val remainingWaypoints = pigeonVar_list[8] as Long? + val upcomingRoadObjects = pigeonVar_list[9] as List? + val stale = pigeonVar_list[10] as Boolean? + val routeAlternativeId = pigeonVar_list[11] as String? + val currentRouteGeometryIndex = pigeonVar_list[12] as Long? + val inParkingAisle = pigeonVar_list[13] as Boolean? + return RouteProgress(bannerInstructionsJson, voiceInstructionsJson, currentState, inTunnel, distanceRemaining, distanceTraveled, durationRemaining, fractionTraveled, remainingWaypoints, upcomingRoadObjects, stale, routeAlternativeId, currentRouteGeometryIndex, inParkingAisle) + } + } + fun toList(): List { + return listOf( + bannerInstructionsJson, + voiceInstructionsJson, + currentState, + inTunnel, + distanceRemaining, + distanceTraveled, + durationRemaining, + fractionTraveled, + remainingWaypoints, + upcomingRoadObjects, + stale, + routeAlternativeId, + currentRouteGeometryIndex, + inParkingAisle, + ) + } +} +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()) + } + } + 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) + } + 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(waypoints: List, 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 waypointsArg = args[0] as List + api.setRoute(waypointsArg) { 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/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 13372aef5e24af05341d49695ee84e5f9b594659..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 53636 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ⅅ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 H-+%o-(+5aQ 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 0000000000000000000000000000000000000000..e58cc3731d03a86a9ef66ac10480a24b4184a062 GIT binary patch literal 3285 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 TUL9mn00000NkvXXu0mjfw98~2 literal 0 HcmV?d00001 diff --git a/example/lib/main.dart b/example/lib/main.dart index 91af025f7..d3762b381 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 { diff --git a/example/lib/navigator_example.dart b/example/lib/navigator_example.dart new file mode 100644 index 000000000..5ca274d3a --- /dev/null +++ b/example/lib/navigator_example.dart @@ -0,0 +1,190 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mapbox_maps_example/main.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 'package:turf/turf.dart' as turf; + +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); + await _start(); + } + + _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 { + 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: 1500)); + } + + Future _start() async { + await Permission.location.request(); + + 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, + locationPuck: LocationPuck( + locationPuck2D: DefaultLocationPuck2D( + topImage: list, + bearingImage: Uint8List.fromList([]), + shadowImage: Uint8List.fromList([]))))); + + 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; + } + + myCoordinate = Position(lastLocation.longitude!, lastLocation.latitude!); + } + + await mapboxMap + .setCamera(CameraOptions(center: Point(coordinates: myCoordinate))); + + final destinationCoordinate = createRandomPositionAround(myCoordinate); + + await mapboxMap.navigation.setRoute([ + Point(coordinates: myCoordinate), + Point(coordinates: destinationCoordinate) + ]); + } + + @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 - 80, + child: mapWidget), + ), + Padding( + padding: const EdgeInsets.all(8), + child: FloatingActionButton( + elevation: 4, + onPressed: _onFollowingClicked, + child: const Icon(Icons.mode_standby), + )), + Padding( + padding: const EdgeInsets.fromLTRB(72, 8, 8, 8), + child: FloatingActionButton( + elevation: 4, + 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/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..cb9085c69 --- /dev/null +++ b/lib/src/pigeons/navigation.dart @@ -0,0 +1,736 @@ +// 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.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? 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 [ + bannerInstructionsJson, + voiceInstructionsJson, + currentState, + inTunnel, + distanceRemaining, + distanceTraveled, + durationRemaining, + fractionTraveled, + remainingWaypoints, + upcomingRoadObjects, + stale, + routeAlternativeId, + currentRouteGeometryIndex, + inParkingAisle, + ]; + } + + static RouteProgress decode(Object result) { + result as List; + return RouteProgress( + bannerInstructionsJson: result[0] as String?, + voiceInstructionsJson: result[1] as String?, + currentState: result[2] as RouteProgressState?, + inTunnel: result[3] as bool?, + distanceRemaining: result[4] as double?, + distanceTraveled: result[5] as double?, + durationRemaining: result[6] as double?, + fractionTraveled: result[7] as double?, + remainingWaypoints: result[8] as int?, + upcomingRoadObjects: + (result[9] as List?)?.cast(), + stale: result[10] as bool?, + routeAlternativeId: result[11] as String?, + currentRouteGeometryIndex: result[12] as int?, + inParkingAisle: result[13] 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 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 { + 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(List waypoints) 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([waypoints]) 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"; +} From e37d50c82c9e47b5b19c23f9ef49d7c3778a51f5 Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Sun, 17 Nov 2024 22:42:07 +0100 Subject: [PATCH 02/33] change log update --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) 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 From 39d51d34b85e59b4bfc40870acb6405ae66cf47b Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Sun, 17 Nov 2024 22:59:18 +0100 Subject: [PATCH 03/33] clean up example --- example/lib/navigator_example.dart | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/example/lib/navigator_example.dart b/example/lib/navigator_example.dart index 5ca274d3a..573e124bc 100644 --- a/example/lib/navigator_example.dart +++ b/example/lib/navigator_example.dart @@ -1,15 +1,10 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:mapbox_maps_example/main.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 'package:turf/turf.dart' as turf; import 'example.dart'; @@ -179,7 +174,7 @@ class NavigatorExampleState extends State Padding( padding: const EdgeInsets.fromLTRB(72, 8, 8, 8), child: FloatingActionButton( - elevation: 4, + elevation: 5, onPressed: _onOverviewClicked, child: const Icon(Icons.route), )), From a6fcedde5bf1f4ab4ceefeabf53cb42ec655206a Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Sun, 17 Nov 2024 23:01:05 +0100 Subject: [PATCH 04/33] fix camera animation duration --- example/lib/navigator_example.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/navigator_example.dart b/example/lib/navigator_example.dart index 573e124bc..2b29d4c01 100644 --- a/example/lib/navigator_example.dart +++ b/example/lib/navigator_example.dart @@ -98,7 +98,7 @@ class NavigatorExampleState extends State pitch: 45, padding: MbxEdgeInsets(top: 300.0, left: 0, bottom: 0, right: 0), bearing: location.bearing), - MapAnimationOptions(duration: 1500)); + MapAnimationOptions(duration: 100)); } Future _start() async { From e298bbaa88bfee5d0478065268673590475a79e5 Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Sun, 17 Nov 2024 23:01:51 +0100 Subject: [PATCH 05/33] add missed await --- example/lib/navigator_example.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/navigator_example.dart b/example/lib/navigator_example.dart index 2b29d4c01..4b405600c 100644 --- a/example/lib/navigator_example.dart +++ b/example/lib/navigator_example.dart @@ -90,7 +90,7 @@ class NavigatorExampleState extends State } Future updateCamera(NavigationLocation location) async { - mapboxMap.easeTo( + await mapboxMap.easeTo( CameraOptions( center: Point( coordinates: Position(location.longitude!, location.latitude!)), From caf528590522de14bf53a770c2ae112bc777896f Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Mon, 30 Dec 2024 15:41:13 +0100 Subject: [PATCH 06/33] ios --- example/ios/Podfile.lock | 16 +- example/ios/Runner/Info.plist | 27 +- example/pubspec.lock | 34 +-- .../Generated/NavigationListeners.swift | 254 ++++++++++++++++++ ios/Classes/Generated/Settings.swift | 50 ++++ ios/Classes/NavigationController.swift | 25 ++ 6 files changed, 368 insertions(+), 38 deletions(-) create mode 100644 ios/Classes/Generated/NavigationListeners.swift create mode 100644 ios/Classes/NavigationController.swift diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 429d47b2a..062d999e1 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -4,14 +4,14 @@ PODS: - Flutter - mapbox_maps_flutter (2.4.0): - Flutter - - MapboxMaps (~> 11.8.0) + - MapboxMaps (~> 11.9.0) - Turf (= 3.0.0) - - MapboxCommon (24.8.0) - - MapboxCoreMaps (11.8.0): - - MapboxCommon (~> 24.8) - - MapboxMaps (11.8.0): - - MapboxCommon (= 24.8.0) - - MapboxCoreMaps (= 11.8.0) + - MapboxCommon (24.9.0) + - MapboxCoreMaps (11.9.0): + - MapboxCommon (~> 24.9) + - MapboxMaps (11.9.0): + - MapboxCommon (= 24.9.0) + - MapboxCoreMaps (= 11.9.0) - Turf (= 3.0.0) - path_provider_foundation (0.0.1): - Flutter @@ -59,4 +59,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: e9395e37b54f3250ebce302f8de7800b2ba2b828 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 93972b79d..8f8e35d8d 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 + pk.eyJ1IjoicmlkZWhpa2UiLCJhIjoiY2xwc2wwNGZrMDN3eTJqcGwxdjViaGRzdiJ9.jFtcT-N-qh-Zj7i0vrWxAA + 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/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/Generated/NavigationListeners.swift b/ios/Classes/Generated/NavigationListeners.swift new file mode 100644 index 000000000..a58ab8554 --- /dev/null +++ b/ios/Classes/Generated/NavigationListeners.swift @@ -0,0 +1,254 @@ +// Autogenerated from Pigeon (v22.4.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif +import struct Turf.Point + +/// Error class for passing custom error details to Dart side. +final class NavigationListenersError: 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 + "NavigationListenersError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func createConnectionError(withChannelName channelName: String) -> GestureListenersError { + return NavigationListenersError(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? +} + + +private class NavigationListenersPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return GestureState(rawValue: enumResultAsInt) + } + return nil + case 130: + return Point.fromList(self.readValue() as! [Any?]) + case 131: + return ScreenCoordinate.fromList(self.readValue() as! [Any?]) + case 132: + return MapContentGestureContext.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class NavigationListenersPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? GestureState { + super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? Point { + super.writeByte(130) + super.writeValue(value.toList()) + } else if let value = value as? ScreenCoordinate { + super.writeByte(131) + super.writeValue(value.toList()) + } else if let value = value as? MapContentGestureContext { + super.writeByte(132) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class NavigationListenersPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return NavigationListenersPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return NavigationListenersPigeonCodecWriter(data: data) + } +} + +class NavigationListenersPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = NavigationListenersPigeonCodec(readerWriter: GestureListenersPigeonCodecReaderWriter()) +} + +/// 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(NavigationLocation location, completion: @escaping (Result) -> Void) + + func onRouteProgress(RouteProgress routeProgress, completion: @escaping (Result) -> Void) + + func onNavigationCameraStateChanged(NavigationCameraState state, 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: NavigationListenersPigeonCodec { + return NavigationListenersPigeonCodec.shared + } + func onNavigationRouteReadycompletion: @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([] 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(NavigationListenersError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onNavigationRouteFailed: @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([] 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(NavigationListenersError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onNavigationRouteCancelled: @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([] 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(NavigationListenersError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onNavigationRouteRendered: @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([] 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(NavigationListenersError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onNewLocation(NavigationLocation location, 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([location] 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(GestureListenersError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onRouteProgress(RouteProgress 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([routeProgress] 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(GestureListenersError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onNavigationCameraStateChanged(NavigationCameraState state, 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([state] 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(GestureListenersError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } +} diff --git a/ios/Classes/Generated/Settings.swift b/ios/Classes/Generated/Settings.swift index fb0357238..e79acffa3 100644 --- a/ios/Classes/Generated/Settings.swift +++ b/ios/Classes/Generated/Settings.swift @@ -1181,3 +1181,53 @@ class LogoSettingsInterfaceSetup { } } } + +/// Mapbox navigation. +/// +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol NavigationInterface { + fun setRoute(waypoints: List) throws + fun stopTripSession() throws + fun startTripSession(withForegroundService: Boolean) throws + fun requestNavigationCameraToFollowing() throws + fun requestNavigationCameraToOverview() throws + fun lastLocation() throws +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class NavigationInterfaceSetup { + static var codec: FlutterStandardMessageCodec { SettingsPigeonCodec.shared } + /// Sets up an instance of `LogoSettingsInterface` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: LogoSettingsInterface?, 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 { + let args = message as! [Any?] + let waypointsArg = args[0] as! List + setRouteChannel.setMessageHandler { _, reply in + do { + let result = try api.setRoute(waypointsArg) + reply(wrapResult(result)) + } catch { + 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 { message, reply in + do { + try api.stopTripSession() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + stopTripSessionChannel.setMessageHandler(nil) + } + } +} + diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift new file mode 100644 index 000000000..042308a58 --- /dev/null +++ b/ios/Classes/NavigationController.swift @@ -0,0 +1,25 @@ +import Foundation +@_spi(Experimental) import MapboxMaps +import Flutter + +final class NavigationController: NSObject, NavigationInterface { + + private var cancelables: Set = [] + private var onNavigationListener: NavigationListener? + private let mapView: MapView + + init(withMapView mapView: MapView) { + self.mapView = mapView + } + + func addListeners(messenger: SuffixBinaryMessenger) { + removeListeners() + + + onNavigationListener = NavigationListener(binaryMessenger: messenger.messenger, messageChannelSuffix: messenger.suffix) + } + + func removeListeners() { + cancelables = [] + } +} From bb42b9d33b48e4e864b601844d55f6449d3c98e7 Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Mon, 30 Dec 2024 22:23:47 +0100 Subject: [PATCH 07/33] rework navigation --- .../Generated/NavigationListeners.swift | 254 ------- .../Generated/NavigationMessager.swift | 680 ++++++++++++++++++ ios/Classes/Generated/Settings.swift | 52 +- ios/Classes/MapboxMapController.swift | 13 + ios/Classes/NavigationController.swift | 125 +++- 5 files changed, 813 insertions(+), 311 deletions(-) delete mode 100644 ios/Classes/Generated/NavigationListeners.swift create mode 100644 ios/Classes/Generated/NavigationMessager.swift diff --git a/ios/Classes/Generated/NavigationListeners.swift b/ios/Classes/Generated/NavigationListeners.swift deleted file mode 100644 index a58ab8554..000000000 --- a/ios/Classes/Generated/NavigationListeners.swift +++ /dev/null @@ -1,254 +0,0 @@ -// Autogenerated from Pigeon (v22.4.0), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif -import struct Turf.Point - -/// Error class for passing custom error details to Dart side. -final class NavigationListenersError: 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 - "NavigationListenersError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" - } -} - -private func createConnectionError(withChannelName channelName: String) -> GestureListenersError { - return NavigationListenersError(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? -} - - -private class NavigationListenersPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) - if let enumResultAsInt = enumResultAsInt { - return GestureState(rawValue: enumResultAsInt) - } - return nil - case 130: - return Point.fromList(self.readValue() as! [Any?]) - case 131: - return ScreenCoordinate.fromList(self.readValue() as! [Any?]) - case 132: - return MapContentGestureContext.fromList(self.readValue() as! [Any?]) - default: - return super.readValue(ofType: type) - } - } -} - -private class NavigationListenersPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? GestureState { - super.writeByte(129) - super.writeValue(value.rawValue) - } else if let value = value as? Point { - super.writeByte(130) - super.writeValue(value.toList()) - } else if let value = value as? ScreenCoordinate { - super.writeByte(131) - super.writeValue(value.toList()) - } else if let value = value as? MapContentGestureContext { - super.writeByte(132) - super.writeValue(value.toList()) - } else { - super.writeValue(value) - } - } -} - -private class NavigationListenersPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return NavigationListenersPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return NavigationListenersPigeonCodecWriter(data: data) - } -} - -class NavigationListenersPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = NavigationListenersPigeonCodec(readerWriter: GestureListenersPigeonCodecReaderWriter()) -} - -/// 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(NavigationLocation location, completion: @escaping (Result) -> Void) - - func onRouteProgress(RouteProgress routeProgress, completion: @escaping (Result) -> Void) - - func onNavigationCameraStateChanged(NavigationCameraState state, 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: NavigationListenersPigeonCodec { - return NavigationListenersPigeonCodec.shared - } - func onNavigationRouteReadycompletion: @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([] 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(NavigationListenersError(code: code, message: message, details: details))) - } else { - completion(.success(Void())) - } - } - } - func onNavigationRouteFailed: @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([] 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(NavigationListenersError(code: code, message: message, details: details))) - } else { - completion(.success(Void())) - } - } - } - func onNavigationRouteCancelled: @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([] 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(NavigationListenersError(code: code, message: message, details: details))) - } else { - completion(.success(Void())) - } - } - } - func onNavigationRouteRendered: @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([] 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(NavigationListenersError(code: code, message: message, details: details))) - } else { - completion(.success(Void())) - } - } - } - func onNewLocation(NavigationLocation location, 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([location] 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(GestureListenersError(code: code, message: message, details: details))) - } else { - completion(.success(Void())) - } - } - } - func onRouteProgress(RouteProgress 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([routeProgress] 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(GestureListenersError(code: code, message: message, details: details))) - } else { - completion(.success(Void())) - } - } - } - func onNavigationCameraStateChanged(NavigationCameraState state, 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([state] 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(GestureListenersError(code: code, message: message, details: details))) - } else { - completion(.success(Void())) - } - } - } -} diff --git a/ios/Classes/Generated/NavigationMessager.swift b/ios/Classes/Generated/NavigationMessager.swift new file mode 100644 index 000000000..7d36f7b39 --- /dev/null +++ b/ios/Classes/Generated/NavigationMessager.swift @@ -0,0 +1,680 @@ +// 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? PigeonError { + 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 PigeonError(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 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 bannerInstructionsJson: String? = nilOrValue(pigeonVar_list[0]) + let voiceInstructionsJson: String? = nilOrValue(pigeonVar_list[1]) + let currentState: RouteProgressState? = nilOrValue(pigeonVar_list[2]) + let inTunnel: Bool? = nilOrValue(pigeonVar_list[3]) + let distanceRemaining: Double? = nilOrValue(pigeonVar_list[4]) + let distanceTraveled: Double? = nilOrValue(pigeonVar_list[5]) + let durationRemaining: Double? = nilOrValue(pigeonVar_list[6]) + let fractionTraveled: Double? = nilOrValue(pigeonVar_list[7]) + let remainingWaypoints: Int64? = nilOrValue(pigeonVar_list[8]) + let upcomingRoadObjects: [UpcomingRoadObject]? = nilOrValue(pigeonVar_list[9]) + let stale: Bool? = nilOrValue(pigeonVar_list[10]) + let routeAlternativeId: String? = nilOrValue(pigeonVar_list[11]) + let currentRouteGeometryIndex: Int64? = nilOrValue(pigeonVar_list[12]) + let inParkingAisle: Bool? = nilOrValue(pigeonVar_list[13]) + + return RouteProgress( + 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 [ + bannerInstructionsJson, + voiceInstructionsJson, + currentState, + inTunnel, + distanceRemaining, + distanceTraveled, + durationRemaining, + fractionTraveled, + remainingWaypoints, + upcomingRoadObjects, + stale, + routeAlternativeId, + currentRouteGeometryIndex, + inParkingAisle, + ] + } +} + +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 + 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 { + 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(PigeonError(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(PigeonError(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(PigeonError(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(PigeonError(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(PigeonError(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(PigeonError(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(PigeonError(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(waypoints: [Point], 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 waypointsArg = args[0] as! [Point] + api.setRoute(waypoints: waypointsArg) { 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 e79acffa3..8c704eb05 100644 --- a/ios/Classes/Generated/Settings.swift +++ b/ios/Classes/Generated/Settings.swift @@ -1180,54 +1180,4 @@ class LogoSettingsInterfaceSetup { updateSettingsChannel.setMessageHandler(nil) } } -} - -/// Mapbox navigation. -/// -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol NavigationInterface { - fun setRoute(waypoints: List) throws - fun stopTripSession() throws - fun startTripSession(withForegroundService: Boolean) throws - fun requestNavigationCameraToFollowing() throws - fun requestNavigationCameraToOverview() throws - fun lastLocation() throws -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class NavigationInterfaceSetup { - static var codec: FlutterStandardMessageCodec { SettingsPigeonCodec.shared } - /// Sets up an instance of `LogoSettingsInterface` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: LogoSettingsInterface?, 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 { - let args = message as! [Any?] - let waypointsArg = args[0] as! List - setRouteChannel.setMessageHandler { _, reply in - do { - let result = try api.setRoute(waypointsArg) - reply(wrapResult(result)) - } catch { - 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 { message, reply in - do { - try api.stopTripSession() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - stopTripSessionChannel.setMessageHandler(nil) - } - } -} - +} \ No newline at end of file diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index f5e88d8ca..65cf673fc 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -13,6 +13,7 @@ final class MapboxMapController: NSObject, FlutterPlatformView { private let channel: FlutterMethodChannel private let annotationController: AnnotationController? private let gesturesController: GesturesController? + private let navigationController: NavigationController? private let eventHandler: MapboxEventHandler private let binaryMessenger: SuffixBinaryMessenger @@ -82,6 +83,9 @@ final class MapboxMapController: NSObject, FlutterPlatformView { annotationController = AnnotationController(withMapView: mapView) annotationController!.setup(binaryMessenger: binaryMessenger) + navigationController = NavigationController(withMapView: mapView) + NavigationInterfaceSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: attributionController, messageChannelSuffix: binaryMessenger.suffix) + super.init() channel.setMethodCallHandler { [weak self] in self?.onMethodCall(methodCall: $0, result: $1) } @@ -109,6 +113,14 @@ final class MapboxMapController: NSObject, FlutterPlatformView { } catch { result(FlutterError(code: "2342345", message: error.localizedDescription, details: nil)) } + case "navigation#add_listeners": { + navigationController!.addListeners(messenger: messenger) + result(nil) + } + case "navigation#remove_listeners": { + navigationController!.removeListeners() + result(nil) + } default: result(FlutterMethodNotImplemented) } @@ -128,5 +140,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/NavigationController.swift b/ios/Classes/NavigationController.swift index 042308a58..b09012978 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -1,8 +1,23 @@ -import Foundation -@_spi(Experimental) import MapboxMaps -import Flutter +import Combine +import CoreLocation +import MapboxDirections +import MapboxNavigationCore -final class NavigationController: NSObject, NavigationInterface { +@MainActor +final class NavigationController: ObservableObject, NavigationInterface { + let predictiveCacheManager: PredictiveCacheManager? + + @Published private(set) var isInActiveNavigation: Bool = false + @Published private(set) var currentPreviewRoutes: NavigationRoutes? + @Published private(set) var activeNavigationRoutes: NavigationRoutes? + @Published private(set) var routeProgress: RouteProgress? + @Published private(set) var currentLocation: CLLocation? + @Published var cameraState: NavigationCameraState = .idle + @Published var profileIdentifier: ProfileIdentifier = .automobileAvoidingTraffic + @Published var shouldRequestMapMatching = false + + private var waypoints: [Waypoint] = [] + private let core: MapboxNavigation private var cancelables: Set = [] private var onNavigationListener: NavigationListener? @@ -10,16 +25,114 @@ final class NavigationController: NSObject, NavigationInterface { init(withMapView mapView: MapView) { self.mapView = mapView + + let config = CoreConfig( + credentials: .init(), // You can pass a custom token if you need to, + locationSource: .live + ) + let navigationProvider = MapboxNavigationProvider(coreConfig: config) + self.core = navigationProvider.mapboxNavigation + self.predictiveCacheManager = navigationProvider.predictiveCacheManager + self.observeNavigation() } - func addListeners(messenger: SuffixBinaryMessenger) { - removeListeners() + private func observeNavigation() { + core.tripSession().session + .map { + if case .activeGuidance = $0.state { return true } + return false + } + .removeDuplicates() + .assign(to: &$isInActiveNavigation) + + core.navigation().routeProgress + .map { $0?.routeProgress } + .assign(to: &$routeProgress) + + core.tripSession().navigationRoutes + .assign(to: &$activeNavigationRoutes) + + core.navigation().locationMatching + .map { $0.enhancedLocation } + .assign(to: &$currentLocation) + } + + func startFreeDrive() { + core.tripSession().startFreeDrive() + } + + func cancelPreview() { + waypoints = [] + currentPreviewRoutes = nil + cameraState = .following + } + + func startActiveNavigation() { + guard let previewRoutes = currentPreviewRoutes else { return } + core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) + cameraState = .following + currentPreviewRoutes = nil + waypoints = [] + } + + func stopActiveNavigation() { + core.tripSession().startFreeDrive() + cameraState = .following + } + + func requestRoutes(points: [Point]) async throws { + + waypoints.append(Waypoint(coordinate: mapPoint.coordinate, name: mapPoint.name)) + let provider = core.routingProvider() + if shouldRequestMapMatching { + let mapMatchingOptions = NavigationMatchOptions( + waypoints: optionsWaypoints, + profileIdentifier: profileIdentifier + ) + let previewRoutes = try await provider.calculateRoutes(options: mapMatchingOptions).value + currentPreviewRoutes = previewRoutes + } else { + let routeOptions = NavigationRouteOptions( + waypoints: optionsWaypoints, + profileIdentifier: profileIdentifier + ) + let previewRoutes = try await provider.calculateRoutes(options: routeOptions).value + currentPreviewRoutes = previewRoutes + } + cameraState = .idle + } + func addListeners(messenger: SuffixBinaryMessenger) { + removeListeners() onNavigationListener = NavigationListener(binaryMessenger: messenger.messenger, messageChannelSuffix: messenger.suffix) } func removeListeners() { cancelables = [] } + + func setRoute(waypoints: [Point], completion: @escaping (Result) -> Void) { + self.requestRoutes(waypoints) + } + + func stopTripSession(completion: @escaping (Result) -> Void) { + core.tripSession() + } + + 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) { + + } } From e6e5583b175b7a0a45c9cd9598627e18c445833c Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Mon, 30 Dec 2024 22:57:44 +0100 Subject: [PATCH 08/33] navigation add listener --- ios/Classes/MapboxMapController.swift | 6 ++---- ios/Classes/NavigationController.swift | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index 65cf673fc..f8a8ba92d 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -113,14 +113,12 @@ final class MapboxMapController: NSObject, FlutterPlatformView { } catch { result(FlutterError(code: "2342345", message: error.localizedDescription, details: nil)) } - case "navigation#add_listeners": { + case "navigation#add_listeners": navigationController!.addListeners(messenger: messenger) result(nil) - } - case "navigation#remove_listeners": { + case "navigation#remove_listeners": navigationController!.removeListeners() result(nil) - } default: result(FlutterMethodNotImplemented) } diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index b09012978..2de828e86 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -3,8 +3,7 @@ import CoreLocation import MapboxDirections import MapboxNavigationCore -@MainActor -final class NavigationController: ObservableObject, NavigationInterface { +final class NavigationController: NSObject, NavigationInterface { let predictiveCacheManager: PredictiveCacheManager? @Published private(set) var isInActiveNavigation: Bool = false From 5398d4fe0d2fd413d3b6333b65710ad8b135f757 Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Fri, 3 Jan 2025 15:51:32 +0100 Subject: [PATCH 09/33] podspec with navigation --- example/ios/Podfile | 2 +- example/ios/Podfile.lock | 28 +++---------- example/ios/Runner.xcodeproj/project.pbxproj | 41 ++++++++++++++++++++ ios/Classes/NavigationController.swift | 24 ++++++++---- ios/mapbox_maps_flutter.podspec | 19 ++++++++- 5 files changed, 81 insertions(+), 33 deletions(-) diff --git a/example/ios/Podfile b/example/ios/Podfile index e85ddf35f..458433a0f 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -43,4 +43,4 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end -end +end \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 062d999e1..0f7ab12b7 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -4,21 +4,14 @@ PODS: - Flutter - mapbox_maps_flutter (2.4.0): - Flutter - - MapboxMaps (~> 11.9.0) - - Turf (= 3.0.0) - - MapboxCommon (24.9.0) - - MapboxCoreMaps (11.9.0): - - MapboxCommon (~> 24.9) - - MapboxMaps (11.9.0): - - MapboxCommon (= 24.9.0) - - MapboxCoreMaps (= 11.9.0) - - Turf (= 3.0.0) + - mapbox_maps_flutter/MapboxNavigation (= 2.4.0) + - mapbox_maps_flutter/MapboxNavigation (2.4.0): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter - - Turf (3.0.0) DEPENDENCIES: - Flutter (from `Flutter`) @@ -27,13 +20,6 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) -SPEC REPOS: - trunk: - - MapboxCommon - - MapboxCoreMaps - - MapboxMaps - - Turf - EXTERNAL SOURCES: Flutter: :path: Flutter @@ -49,14 +35,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - mapbox_maps_flutter: 564903a43401bad6b277ffef34b0ab03329a34c1 - MapboxCommon: 95fe03b74d0d0ca39dc646ca14862deb06875151 - MapboxCoreMaps: f2a82182c5f6c6262220b81547c6df708012932b - MapboxMaps: dbe1869006c5918d62efc6b475fb884947ea2ecd + mapbox_maps_flutter: 08c5799a58cdb291e0c617bd3a88b098a31fb85c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - Turf: a1604e74adce15c58462c9ae2acdbf049d5be35e -PODFILE CHECKSUM: e9395e37b54f3250ebce302f8de7800b2ba2b828 +PODFILE CHECKSUM: 0056957a67665f83950e0d86a720a38d6cd25fc3 COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 29d453e22..0f9780c02 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -15,6 +15,10 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; D0F2C3AA8D6C415312599A8A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 024AD1B161F3640150E7C413 /* Pods_Runner.framework */; }; + D7C97D6C2D25EE48006272D0 /* MapboxNavigationCore in Frameworks */ = {isa = PBXBuildFile; productRef = D7C97D6B2D25EE48006272D0 /* MapboxNavigationCore */; }; + D7C97D6E2D25EE48006272D0 /* MapboxNavigationUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = D7C97D6D2D25EE48006272D0 /* MapboxNavigationUIKit */; }; + D7C97D702D25EE48006272D0 /* _MapboxNavigationTestKit in Frameworks */ = {isa = PBXBuildFile; productRef = D7C97D6F2D25EE48006272D0 /* _MapboxNavigationTestKit */; }; + D7C97D722D25EE48006272D0 /* mapbox-directions-swift in Frameworks */ = {isa = PBXBuildFile; productRef = D7C97D712D25EE48006272D0 /* mapbox-directions-swift */; }; FE428FE79EF59766F88E1D91 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50F12F718323ADA4DE282D4D /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ @@ -80,7 +84,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D7C97D6E2D25EE48006272D0 /* MapboxNavigationUIKit in Frameworks */, + D7C97D6C2D25EE48006272D0 /* MapboxNavigationCore in Frameworks */, + D7C97D702D25EE48006272D0 /* _MapboxNavigationTestKit in Frameworks */, D0F2C3AA8D6C415312599A8A /* Pods_Runner.framework in Frameworks */, + D7C97D722D25EE48006272D0 /* mapbox-directions-swift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -236,6 +244,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + D77655142D23F5FA00AA6D82 /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -776,6 +787,36 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + D77655142D23F5FA00AA6D82 /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mapbox/mapbox-navigation-ios.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.6.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + D7C97D6B2D25EE48006272D0 /* MapboxNavigationCore */ = { + isa = XCSwiftPackageProductDependency; + productName = MapboxNavigationCore; + }; + D7C97D6D2D25EE48006272D0 /* MapboxNavigationUIKit */ = { + isa = XCSwiftPackageProductDependency; + productName = MapboxNavigationUIKit; + }; + D7C97D6F2D25EE48006272D0 /* _MapboxNavigationTestKit */ = { + isa = XCSwiftPackageProductDependency; + productName = _MapboxNavigationTestKit; + }; + D7C97D712D25EE48006272D0 /* mapbox-directions-swift */ = { + isa = XCSwiftPackageProductDependency; + productName = "mapbox-directions-swift"; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index 2de828e86..8d2951c3d 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -1,7 +1,7 @@ import Combine import CoreLocation import MapboxDirections -import MapboxNavigationCore +import MapboxCoreNavigation final class NavigationController: NSObject, NavigationInterface { let predictiveCacheManager: PredictiveCacheManager? @@ -113,25 +113,35 @@ final class NavigationController: NSObject, NavigationInterface { func setRoute(waypoints: [Point], completion: @escaping (Result) -> Void) { self.requestRoutes(waypoints) + completion(.success()) } func stopTripSession(completion: @escaping (Result) -> Void) { - core.tripSession() + //core.cameraState + cameraState = .overview + completion(.success()) } func startTripSession(withForegroundService: Bool, completion: @escaping (Result) -> Void) { - + guard let previewRoutes = currentPreviewRoutes else { return } + core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) + cameraState = .following + currentPreviewRoutes = nil + waypoints = [] + completion(.success()) } func requestNavigationCameraToFollowing(completion: @escaping (Result) -> Void) { - + cameraState = .following + completion(.success()) } func requestNavigationCameraToOverview(completion: @escaping (Result) -> Void) { - + cameraState = .overview + completion(.success()) } - func lastLocation(completion: @escaping (Result) -> Void) { - + func lastLocation(completion: @escaping (Result) -> Void) { + completion(.success(nil)) } } diff --git a/ios/mapbox_maps_flutter.podspec b/ios/mapbox_maps_flutter.podspec index 25f6536b3..555e88cd9 100644 --- a/ios/mapbox_maps_flutter.podspec +++ b/ios/mapbox_maps_flutter.podspec @@ -2,6 +2,17 @@ # 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 |nav| + nav.name = 'MapboxNavigation' + nav.version = '3.6.0' + nav.summary = 'Mapbox Navigation SDK.' + nav.author = { 'Mapbox' => 'mobile@mapbox.com' } + nav.source = { :git => "https://github.com/mapbox/mapbox-navigation-ios.git", :tag => "3.6.0" } + + nav.dependency 'MapboxMaps', '11.9.0' + nav.dependency 'Turf', '~> 3.0.0' +end + Pod::Spec.new do |s| s.name = 'mapbox_maps_flutter' s.version = '2.4.0' @@ -17,8 +28,12 @@ Pod::Spec.new do |s| s.dependency 'Flutter' s.platform = :ios, '12.0' - s.dependency 'MapboxMaps', '~> 11.8.0' - s.dependency 'Turf', '3.0.0' + #s.dependency 'MapboxMaps', '11.6.0' + #s.dependency 'Turf', '~> 2.8.0' + + #s.dependency 'MapboxNavigation' + s.subspec 'MapboxNavigation' do |nav_module| + end # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } From 90a454554f5f05348ed34fb19bfe0a3ebfbfc445 Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sat, 4 Jan 2025 19:43:30 +0100 Subject: [PATCH 10/33] fix pods --- .gitignore | 2 ++ example/ios/Podfile | 4 +++- example/ios/Podfile.lock | 33 +++++++++++++++++++++++++++----- ios/MapboxNavigationCore.podspec | 26 +++++++++++++++++++++++++ ios/mapbox_maps_flutter.podspec | 20 +++---------------- 5 files changed, 62 insertions(+), 23 deletions(-) create mode 100644 ios/MapboxNavigationCore.podspec diff --git a/.gitignore b/.gitignore index fee93470e..0660e8993 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,5 @@ app.*.symbols !**/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/example/ios/Podfile b/example/ios/Podfile index 458433a0f..312d15c36 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -43,4 +43,6 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end -end \ No newline at end of file +end + +pod 'MapboxNavigationCore', :podspec => '../../ios/MapboxNavigationCore.podspec' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 0f7ab12b7..6b59a3ef7 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -4,22 +4,38 @@ PODS: - Flutter - mapbox_maps_flutter (2.4.0): - Flutter - - mapbox_maps_flutter/MapboxNavigation (= 2.4.0) - - mapbox_maps_flutter/MapboxNavigation (2.4.0): - - Flutter + - MapboxMaps (= 11.8.0) + - Turf (= 3.0.0) + - MapboxCommon (24.8.0) + - MapboxCoreMaps (11.8.0): + - MapboxCommon (~> 24.8) + - MapboxMaps (11.8.0): + - MapboxCommon (= 24.8.0) + - MapboxCoreMaps (= 11.8.0) + - Turf (= 3.0.0) + - MapboxNavigationCore (3.5.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter + - Turf (3.0.0) DEPENDENCIES: - Flutter (from `Flutter`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - mapbox_maps_flutter (from `.symlinks/plugins/mapbox_maps_flutter/ios`) + - MapboxNavigationCore (from `../../ios/MapboxNavigationCore.podspec`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) +SPEC REPOS: + trunk: + - MapboxCommon + - MapboxCoreMaps + - MapboxMaps + - Turf + EXTERNAL SOURCES: Flutter: :path: Flutter @@ -27,6 +43,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" mapbox_maps_flutter: :path: ".symlinks/plugins/mapbox_maps_flutter/ios" + MapboxNavigationCore: + :podspec: "../../ios/MapboxNavigationCore.podspec" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: @@ -35,10 +53,15 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - mapbox_maps_flutter: 08c5799a58cdb291e0c617bd3a88b098a31fb85c + mapbox_maps_flutter: f6f8f755eb2ca0bd1596632a8fef81bddbee532c + MapboxCommon: 95fe03b74d0d0ca39dc646ca14862deb06875151 + MapboxCoreMaps: f2a82182c5f6c6262220b81547c6df708012932b + MapboxMaps: dbe1869006c5918d62efc6b475fb884947ea2ecd + MapboxNavigationCore: d1c8ae5c5931d4be6b277e1e1cc3b0e586b03ed2 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + Turf: a1604e74adce15c58462c9ae2acdbf049d5be35e -PODFILE CHECKSUM: 0056957a67665f83950e0d86a720a38d6cd25fc3 +PODFILE CHECKSUM: 424f8ef03c18c39bf6fc40d339073180ee3c7290 COCOAPODS: 1.16.2 diff --git a/ios/MapboxNavigationCore.podspec b/ios/MapboxNavigationCore.podspec new file mode 100644 index 000000000..18ab786a4 --- /dev/null +++ b/ios/MapboxNavigationCore.podspec @@ -0,0 +1,26 @@ +# +# 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 |nav| + nav.name = 'MapboxNavigationCore' + nav.version = '3.5.0' + nav.summary = 'Mapbox Navigation SDK.' + nav.author = { 'Mapbox' => 'mobile@mapbox.com' } + nav.source = { :git => "https://github.com/mapbox/mapbox-navigation-ios.git", :tag => "v3.5.0" } + + nav.homepage = 'https://github.com/mapbox/mapbox-navigation-ios' + nav.license = 'MIT' + nav.author = { 'Mapbox' => 'mobile@mapbox.com' } + + #nav.dependency 'MapboxMaps', '11.8.0' + #nav.dependency 'Turf', '~> 3.0.0' + #nav.dependency 'MapboxDirections', '~> 2.14' + nav.source_files = 'Sources/MapboxNavigationCore/**/*.swift' + + s.platform = :ios, '12.0' + + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.8' +end \ No newline at end of file diff --git a/ios/mapbox_maps_flutter.podspec b/ios/mapbox_maps_flutter.podspec index 555e88cd9..b1eba4f2b 100644 --- a/ios/mapbox_maps_flutter.podspec +++ b/ios/mapbox_maps_flutter.podspec @@ -2,16 +2,6 @@ # 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 |nav| - nav.name = 'MapboxNavigation' - nav.version = '3.6.0' - nav.summary = 'Mapbox Navigation SDK.' - nav.author = { 'Mapbox' => 'mobile@mapbox.com' } - nav.source = { :git => "https://github.com/mapbox/mapbox-navigation-ios.git", :tag => "3.6.0" } - - nav.dependency 'MapboxMaps', '11.9.0' - nav.dependency 'Turf', '~> 3.0.0' -end Pod::Spec.new do |s| s.name = 'mapbox_maps_flutter' @@ -28,14 +18,10 @@ Pod::Spec.new do |s| s.dependency 'Flutter' s.platform = :ios, '12.0' - #s.dependency 'MapboxMaps', '11.6.0' - #s.dependency 'Turf', '~> 2.8.0' - - #s.dependency 'MapboxNavigation' - s.subspec 'MapboxNavigation' do |nav_module| - end - # 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' end From fad16f99d47fa3f074b7fb99a514b3f049c06e60 Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Wed, 8 Jan 2025 15:41:41 +0100 Subject: [PATCH 11/33] pod spec --- example/ios/Flutter/AppFrameworkInfo.plist | 2 +- example/ios/Podfile | 6 +- example/ios/Podfile.lock | 28 +++++++-- example/ios/Runner.xcodeproj/project.pbxproj | 45 +------------- ios/MapboxDirections.podspec | 28 +++++++++ ios/MapboxNavigationCore.podspec | 21 ++++--- ios/Turf.podspec | 45 ++++++++++++++ ios/mapbox_maps_flutter.podspec | 65 +++++++++++++++++++- 8 files changed, 182 insertions(+), 58 deletions(-) create mode 100644 ios/MapboxDirections.podspec create mode 100644 ios/Turf.podspec 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 312d15c36..900f0959b 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, '13.0' use_frameworks! # CocoaPods analytics sends network stats synchronously affecting flutter build latency. @@ -45,4 +45,6 @@ post_install do |installer| end end -pod 'MapboxNavigationCore', :podspec => '../../ios/MapboxNavigationCore.podspec' +pod 'Turf', :podspec => '../../ios/Turf.podspec' +pod 'MapboxDirections', :podspec => '../../ios/MapboxDirections.podspec' +pod 'MapboxNavigationCore', :podspec => '../../ios/MapboxNavigationCore.podspec', :modular_headers => false diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6b59a3ef7..8f5c30fa1 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -3,12 +3,24 @@ PODS: - integration_test (0.0.1): - Flutter - mapbox_maps_flutter (2.4.0): + - Flutter + - mapbox_maps_flutter/MapboxDirections (= 2.4.0) + - mapbox_maps_flutter/MapboxNavigationCore (= 2.4.0) + - MapboxMaps (= 11.8.0) + - Turf (= 3.0.0) + - mapbox_maps_flutter/MapboxDirections (2.4.0): + - Flutter + - MapboxMaps (= 11.8.0) + - Turf (= 3.0.0) + - mapbox_maps_flutter/MapboxNavigationCore (2.4.0): - Flutter - MapboxMaps (= 11.8.0) - Turf (= 3.0.0) - MapboxCommon (24.8.0) - MapboxCoreMaps (11.8.0): - MapboxCommon (~> 24.8) + - MapboxDirections (3.5.0): + - Turf (= 3.0.0) - MapboxMaps (11.8.0): - MapboxCommon (= 24.8.0) - MapboxCoreMaps (= 11.8.0) @@ -25,16 +37,17 @@ DEPENDENCIES: - Flutter (from `Flutter`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - mapbox_maps_flutter (from `.symlinks/plugins/mapbox_maps_flutter/ios`) + - MapboxDirections (from `../../ios/MapboxDirections.podspec`) - MapboxNavigationCore (from `../../ios/MapboxNavigationCore.podspec`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - Turf (from `../../ios/Turf.podspec`) SPEC REPOS: trunk: - MapboxCommon - MapboxCoreMaps - MapboxMaps - - Turf EXTERNAL SOURCES: Flutter: @@ -43,25 +56,30 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" mapbox_maps_flutter: :path: ".symlinks/plugins/mapbox_maps_flutter/ios" + MapboxDirections: + :podspec: "../../ios/MapboxDirections.podspec" MapboxNavigationCore: :podspec: "../../ios/MapboxNavigationCore.podspec" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + Turf: + :podspec: "../../ios/Turf.podspec" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - mapbox_maps_flutter: f6f8f755eb2ca0bd1596632a8fef81bddbee532c + mapbox_maps_flutter: 4966dceac0f20eb390b78ac328bfd6fbe7585021 MapboxCommon: 95fe03b74d0d0ca39dc646ca14862deb06875151 MapboxCoreMaps: f2a82182c5f6c6262220b81547c6df708012932b + MapboxDirections: 241eac54a7ec44425e80b1a06d5a24db111014cb MapboxMaps: dbe1869006c5918d62efc6b475fb884947ea2ecd - MapboxNavigationCore: d1c8ae5c5931d4be6b277e1e1cc3b0e586b03ed2 + MapboxNavigationCore: c256657b4313e10c62f95568029126b65a2261ea path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - Turf: a1604e74adce15c58462c9ae2acdbf049d5be35e + Turf: c4e870016295bce16600f33aa398a2776ed5f6e9 -PODFILE CHECKSUM: 424f8ef03c18c39bf6fc40d339073180ee3c7290 +PODFILE CHECKSUM: 98334460b64066f2bc0ce2995da4713f6a0238e5 COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 0f9780c02..deb8905ed 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -15,10 +15,6 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; D0F2C3AA8D6C415312599A8A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 024AD1B161F3640150E7C413 /* Pods_Runner.framework */; }; - D7C97D6C2D25EE48006272D0 /* MapboxNavigationCore in Frameworks */ = {isa = PBXBuildFile; productRef = D7C97D6B2D25EE48006272D0 /* MapboxNavigationCore */; }; - D7C97D6E2D25EE48006272D0 /* MapboxNavigationUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = D7C97D6D2D25EE48006272D0 /* MapboxNavigationUIKit */; }; - D7C97D702D25EE48006272D0 /* _MapboxNavigationTestKit in Frameworks */ = {isa = PBXBuildFile; productRef = D7C97D6F2D25EE48006272D0 /* _MapboxNavigationTestKit */; }; - D7C97D722D25EE48006272D0 /* mapbox-directions-swift in Frameworks */ = {isa = PBXBuildFile; productRef = D7C97D712D25EE48006272D0 /* mapbox-directions-swift */; }; FE428FE79EF59766F88E1D91 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50F12F718323ADA4DE282D4D /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ @@ -84,11 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D7C97D6E2D25EE48006272D0 /* MapboxNavigationUIKit in Frameworks */, - D7C97D6C2D25EE48006272D0 /* MapboxNavigationCore in Frameworks */, - D7C97D702D25EE48006272D0 /* _MapboxNavigationTestKit in Frameworks */, D0F2C3AA8D6C415312599A8A /* Pods_Runner.framework in Frameworks */, - D7C97D722D25EE48006272D0 /* mapbox-directions-swift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -245,7 +237,6 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - D77655142D23F5FA00AA6D82 /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -562,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 = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -643,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 = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -692,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 = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -787,36 +778,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - D77655142D23F5FA00AA6D82 /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/mapbox/mapbox-navigation-ios.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.6.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - D7C97D6B2D25EE48006272D0 /* MapboxNavigationCore */ = { - isa = XCSwiftPackageProductDependency; - productName = MapboxNavigationCore; - }; - D7C97D6D2D25EE48006272D0 /* MapboxNavigationUIKit */ = { - isa = XCSwiftPackageProductDependency; - productName = MapboxNavigationUIKit; - }; - D7C97D6F2D25EE48006272D0 /* _MapboxNavigationTestKit */ = { - isa = XCSwiftPackageProductDependency; - productName = _MapboxNavigationTestKit; - }; - D7C97D712D25EE48006272D0 /* mapbox-directions-swift */ = { - isa = XCSwiftPackageProductDependency; - productName = "mapbox-directions-swift"; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/MapboxDirections.podspec b/ios/MapboxDirections.podspec new file mode 100644 index 000000000..8ef9ac37b --- /dev/null +++ b/ios/MapboxDirections.podspec @@ -0,0 +1,28 @@ +# +# 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 |md| + md.name = 'MapboxDirections' + md.version = '3.5.0' + md.summary = 'Mapbox Directions.' + md.author = { 'Mapbox' => 'mobile@mapbox.com' } + md.source = { :git => "https://github.com/mapbox/mapbox-navigation-ios.git", :tag => "v3.5.0" } + + md.homepage = 'https://github.com/mapbox/mapbox-navigation-ios' + md.license = 'MIT' + md.author = { 'Mapbox' => 'mobile@mapbox.com' } + + md.source_files = 'Sources/MapboxDirections/**/*.{h,m,swift}' + + md.requires_arc = true + md.module_name = "MapboxDirections" + + md.platform = :ios, '13.0' + + md.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + md.swift_version = '5.8' + + md.dependency 'Turf', '3.0.0' +end \ No newline at end of file diff --git a/ios/MapboxNavigationCore.podspec b/ios/MapboxNavigationCore.podspec index 18ab786a4..0cdbe834d 100644 --- a/ios/MapboxNavigationCore.podspec +++ b/ios/MapboxNavigationCore.podspec @@ -14,13 +14,20 @@ Pod::Spec.new do |nav| nav.license = 'MIT' nav.author = { 'Mapbox' => 'mobile@mapbox.com' } - #nav.dependency 'MapboxMaps', '11.8.0' - #nav.dependency 'Turf', '~> 3.0.0' - #nav.dependency 'MapboxDirections', '~> 2.14' - nav.source_files = 'Sources/MapboxNavigationCore/**/*.swift' + nav.source_files = 'Sources/MapboxNavigationCore/**/*.{h,m,swift}' + nav.resource_bundle = { 'MapboxCoreNavigationResources' => ['Sources/MapboxCoreNavigation/Resources/*/*', 'Sources/MapboxCoreNavigation/Resources/*'] } - s.platform = :ios, '12.0' + nav.requires_arc = true + nav.module_name = "MapboxCoreNavigation" - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.8' + nav.platform = :ios, '13.0' + + nav.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + nav.swift_version = '5.8' + + nav.dependency 'Turf', '3.0.0' + #nav.dependency 'MapboxDirections', '3.5.0' + + nav.subspec 'MapboxDirections' do |directions| + end end \ No newline at end of file diff --git a/ios/Turf.podspec b/ios/Turf.podspec new file mode 100644 index 000000000..b5c9933c8 --- /dev/null +++ b/ios/Turf.podspec @@ -0,0 +1,45 @@ +Pod::Spec.new do |s| + + # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + + s.name = "Turf" + s.version = "3.0.0" + s.summary = "Simple spatial analysis." + s.description = "A spatial analysis library written in Swift for native iOS, macOS, tvOS, watchOS, visionOS, and Linux applications, ported from Turf.js." + + s.homepage = "https://github.com/mapbox/turf-swift" + + # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + + s.license = { :type => "ISC", :file => "LICENSE.md" } + + # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + + s.author = { "Mapbox" => "mobile@mapbox.com" } + s.social_media_url = "https://twitter.com/mapbox" + + # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + + s.ios.deployment_target = "11.0" + s.osx.deployment_target = "10.13" + s.tvos.deployment_target = "11.0" + s.watchos.deployment_target = "4.0" + # CocoaPods doesn't support releasing of visionOS pods yet, need to wait for v1.15.0 release of CocoaPods + # with this fix https://github.com/CocoaPods/CocoaPods/pull/12159. + # s.visionos.deployment_target = "1.0" + + # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + + s.source = { + :http => "https://github.com/mapbox/turf-swift/releases/download/v#{s.version}/Turf.xcframework.zip" + } + + # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + + s.requires_arc = true + s.module_name = "Turf" + s.frameworks = 'CoreLocation' + s.swift_version = "5.7" + s.vendored_frameworks = 'Turf.xcframework' + +end \ No newline at end of file diff --git a/ios/mapbox_maps_flutter.podspec b/ios/mapbox_maps_flutter.podspec index b1eba4f2b..4fdc3fe5e 100644 --- a/ios/mapbox_maps_flutter.podspec +++ b/ios/mapbox_maps_flutter.podspec @@ -2,6 +2,63 @@ # 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 |md| + md.name = 'MapboxDirections' + md.version = '3.5.0' + md.summary = 'Mapbox Directions.' + md.author = { 'Mapbox' => 'mobile@mapbox.com' } + md.source = { :git => "https://github.com/mapbox/mapbox-navigation-ios.git", :tag => "v3.5.0" } + + md.homepage = 'https://github.com/mapbox/mapbox-navigation-ios' + md.license = 'MIT' + md.author = { 'Mapbox' => 'mobile@mapbox.com' } + + md.source_files = 'Sources/MapboxDirections/**/*.{h,m,swift}' + + md.requires_arc = true + md.module_name = "MapboxDirections" + + md.platform = :ios, '13.0' + + md.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + md.swift_version = '5.8' + + md.dependency 'Turf', '3.0.0' +end + +Pod::Spec.new do |nav| + nav.name = 'MapboxNavigationCore' + nav.version = '3.5.0' + nav.summary = 'Mapbox Navigation SDK.' + nav.author = { 'Mapbox' => 'mobile@mapbox.com' } + nav.source = { :git => "https://github.com/mapbox/mapbox-navigation-ios.git", :tag => "v3.5.0" } + + nav.homepage = 'https://github.com/mapbox/mapbox-navigation-ios' + nav.license = 'MIT' + nav.author = { 'Mapbox' => 'mobile@mapbox.com' } + + nav.source_files = 'Sources/MapboxNavigationCore/**/*.{h,m,swift}' + nav.resource_bundle = { 'MapboxCoreNavigationResources' => ['Sources/MapboxCoreNavigation/Resources/*/*', 'Sources/MapboxCoreNavigation/Resources/*'] } + + nav.requires_arc = true + nav.module_name = "MapboxCoreNavigation" + + nav.platform = :ios, '13.0' + + nav.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + nav.swift_version = '5.8' + + nav.dependency 'Turf', '3.0.0' + #nav.dependency 'MapboxDirections', '3.5.0' + + nav.subspec 'MapboxDirections' do |directions| + end +end Pod::Spec.new do |s| s.name = 'mapbox_maps_flutter' @@ -16,7 +73,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '12.0' + s.platform = :ios, '13.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } @@ -24,4 +81,10 @@ Pod::Spec.new do |s| s.dependency 'MapboxMaps', '11.8.0' s.dependency 'Turf', '3.0.0' + + s.subspec 'MapboxDirections' do |directions| + end + + s.subspec 'MapboxNavigationCore' do |nav| + end end From 7a8699e083c5704e9d149ab800276c7db0450ebd Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Thu, 9 Jan 2025 14:32:33 +0100 Subject: [PATCH 12/33] checkout navigation sdk to the classes --- ios/Classes/Navigation/.gitkeep | 0 .../AdministrativeRegion.swift | 43 + .../Navigation/MapboxDirections/Amenity.swift | 50 + .../MapboxDirections/AmenityType.swift | 61 + .../MapboxDirections/AttributeOptions.swift | 154 ++ .../MapboxDirections/BlockedLanes.swift | 134 ++ .../MapboxDirections/Congestion.swift | 33 + .../MapboxDirections/Credentials.swift | 80 + .../CustomValueOptionSet.swift | 675 ++++++++ .../MapboxDirections/Directions.swift | 711 +++++++++ .../MapboxDirections/DirectionsError.swift | 215 +++ .../MapboxDirections/DirectionsOptions.swift | 610 +++++++ .../MapboxDirections/DirectionsResult.swift | 245 +++ .../MapboxDirections/DrivingSide.swift | 12 + .../MapboxDirections/Extensions/Array.swift | 8 + .../MapboxDirections/Extensions/Codable.swift | 84 + .../Extensions/CoreLocation.swift | 31 + .../MapboxDirections/Extensions/Double.swift | 5 + .../Extensions/ForeignMemberContainer.swift | 117 ++ .../MapboxDirections/Extensions/GeoJSON.swift | 58 + .../Extensions/HTTPURLResponse.swift | 30 + .../Extensions/Measurement.swift | 100 ++ .../MapboxDirections/Extensions/String.swift | 7 + .../Extensions/URL+Request.swift | 89 ++ .../MapboxDirections/Incident.swift | 304 ++++ .../MapboxDirections/Interchange.swift | 14 + .../MapboxDirections/Intersection.swift | 483 ++++++ .../MapboxDirections/IsochroneError.swift | 61 + .../MapboxDirections/IsochroneOptions.swift | 272 ++++ .../MapboxDirections/Isochrones.swift | 168 ++ .../MapboxDirections/Junction.swift | 14 + .../Navigation/MapboxDirections/Lane.swift | 57 + .../MapboxDirections/LaneIndication.swift | 134 ++ .../MapMatching/MapMatchingResponse.swift | 87 + .../MapboxDirections/MapMatching/Match.swift | 163 ++ .../MapMatching/MatchOptions.swift | 156 ++ .../MapMatching/Tracepoint.swift | 78 + .../MapboxDirections/MapboxDirections.h | 8 + .../MapboxStreetsRoadClass.swift | 56 + .../Navigation/MapboxDirections/Matrix.swift | 166 ++ .../MapboxDirections/MatrixError.swift | 63 + .../MapboxDirections/MatrixOptions.swift | 204 +++ .../MapboxDirections/MatrixResponse.swift | 128 ++ .../MapboxDirections/OfflineDirections.swift | 134 ++ .../MapboxDirections/Polyline.swift | 397 +++++ .../MapboxDirections/ProfileIdentifier.swift | 48 + .../MapboxDirections/QuickLook.swift | 54 + .../MapboxDirections/RefreshedRoute.swift | 65 + .../ResponseDisposition.swift | 11 + .../MapboxDirections/RestStop.swift | 74 + .../RoadClassExclusionViolation.swift | 16 + .../MapboxDirections/RoadClasses.swift | 175 ++ .../Navigation/MapboxDirections/Route.swift | 130 ++ .../MapboxDirections/RouteLeg.swift | 549 +++++++ .../MapboxDirections/RouteLegAttributes.swift | 147 ++ .../MapboxDirections/RouteOptions.swift | 657 ++++++++ .../RouteRefreshResponse.swift | 88 + .../MapboxDirections/RouteRefreshSource.swift | 63 + .../MapboxDirections/RouteResponse.swift | 393 +++++ .../MapboxDirections/RouteStep.swift | 1112 +++++++++++++ .../MapboxDirections/SilentWaypoint.swift | 48 + .../MapboxDirections/SpokenInstruction.swift | 83 + .../MapboxDirections/TollCollection.swift | 52 + .../MapboxDirections/TollPrice.swift | 147 ++ .../MapboxDirections/TrafficTendency.swift | 20 + .../MapboxDirections/VisualInstruction.swift | 95 ++ .../VisualInstructionBanner.swift | 112 ++ .../VisualInstructionComponent.swift | 345 ++++ .../MapboxDirections/Waypoint.swift | 349 ++++ .../Billing/ApiConfiguration.swift | 78 + .../BillingHandler+SkuTokenProvider.swift | 9 + .../Billing/BillingHandler.swift | 477 ++++++ .../Billing/SkuTokenProvider.swift | 7 + .../Cache/FileCache.swift | 92 ++ .../Cache/SyncBimodalCache.swift | 85 + .../MapboxNavigationCore/CoreConstants.swift | 190 +++ .../MapboxNavigationCore/Environment.swift | 13 + .../Extensions/AVAudioSession.swift | 27 + .../Extensions/AmenityType.swift | 51 + .../Extensions/Array++.swift | 37 + .../Extensions/BoundingBox++.swift | 15 + .../Extensions/Bundle.swift | 63 + .../Extensions/CLLocationDirection++.swift | 9 + .../Extensions/CongestionLevel.swift | 118 ++ .../Extensions/Coordinate2D.swift | 15 + .../Extensions/Date.swift | 20 + .../Extensions/Dictionary.swift | 13 + .../Extensions/FixLocation.swift | 39 + .../Extensions/Geometry.swift | 64 + .../Extensions/Incident.swift | 80 + .../Extensions/Locale.swift | 47 + .../Extensions/MapboxStreetsRoadClass.swift | 34 + .../Extensions/MeasurementSystem.swift | 10 + .../Extensions/NavigationStatus.swift | 36 + .../Extensions/Preconcurrency+Sendable.swift | 7 + .../Extensions/RestStop.swift | 25 + .../Extensions/Result.swift | 20 + .../Extensions/RouteLeg.swift | 46 + .../Extensions/RouteOptions.swift | 62 + .../Extensions/SpokenInstruction.swift | 52 + .../Extensions/String.swift | 58 + .../Extensions/TollCollection.swift | 18 + .../Extensions/UIDevice.swift | 22 + .../Extensions/UIEdgeInsets.swift | 33 + .../Extensions/Utils.swift | 57 + .../ActiveNavigationFeedbackType.swift | 79 + .../Feedback/EventFixLocation.swift | 119 ++ .../Feedback/EventStep.swift | 55 + .../Feedback/EventsManager.swift | 177 +++ .../Feedback/FeedbackEvent.swift | 16 + .../Feedback/FeedbackMetadata.swift | 103 ++ .../Feedback/FeedbackScreenshotOption.swift | 7 + .../Feedback/FeedbackType.swift | 7 + .../NavigationEventsManagerError.swift | 8 + .../PassiveNavigationFeedbackType.swift | 36 + .../Feedback/SearchFeedbackType.swift | 35 + .../History/Copilot/AttachmentsUploader.swift | 152 ++ .../History/Copilot/CopilotService.swift | 99 ++ .../Copilot/Events/ApplicationState.swift | 21 + .../History/Copilot/Events/DriveEnds.swift | 20 + .../History/Copilot/Events/InitRoute.swift | 30 + .../Copilot/Events/NavigationFeedback.swift | 15 + .../Events/NavigationHistoryEvent.swift | 12 + .../Copilot/Events/SearchResultUsed.swift | 62 + .../Copilot/Events/SearchResults.swift | 60 + .../Copilot/FeedbackEventsObserver.swift | 99 ++ .../History/Copilot/MapboxCopilot.swift | 207 +++ .../Copilot/MapboxCopilotDelegate.swift | 14 + .../NavigationHistoryAttachmentProvider.swift | 132 ++ .../NavigationHistoryErrorReport.swift | 35 + .../NavigationHistoryEventsController.swift | 125 ++ .../Copilot/NavigationHistoryFormat.swift | 47 + .../NavigationHistoryLocalStorage.swift | 78 + .../Copilot/NavigationHistoryManager.swift | 115 ++ .../Copilot/NavigationHistoryProvider.swift | 15 + .../Copilot/NavigationHistoryUploader.swift | 70 + .../History/Copilot/NavigationSession.swift | 75 + .../Copilot/Utils/AppEnvironment.swift | 28 + .../History/Copilot/Utils/FileManager++.swift | 27 + .../Copilot/Utils/TokenOwnerProvider.swift | 33 + .../History/Copilot/Utils/TypeConverter.swift | 27 + .../History/HistoryEvent.swift | 82 + .../History/HistoryReader.swift | 198 +++ .../History/HistoryRecorder.swift | 46 + .../History/HistoryRecording.swift | 73 + .../History/HistoryReplayer.swift | 351 ++++ .../IdleTimerManager.swift | 84 + .../Localization/LocalizationManager.swift | 48 + .../Localization/String+Localization.swift | 18 + .../Map/Camera/CameraStateTransition.swift | 34 + .../Map/Camera/FollowingCameraOptions.swift | 260 +++ .../Map/Camera/NavigationCamera.swift | 243 +++ .../Camera/NavigationCameraDebugView.swift | 199 +++ .../Map/Camera/NavigationCameraOptions.swift | 22 + .../Map/Camera/NavigationCameraState.swift | 24 + .../NavigationCameraStateTransition.swift | 209 +++ .../Map/Camera/NavigationCameraType.swift | 8 + .../NavigationViewportDataSourceOptions.swift | 36 + .../Map/Camera/OverviewCameraOptions.swift | 78 + .../CarPlayViewportDataSource.swift | 312 ++++ .../CommonViewportDataSource.swift | 80 + .../MobileViewportDataSource.swift | 341 ++++ .../ViewportDataSource+Calculation.swift | 146 ++ .../ViewportDataSource.swift | 58 + .../ViewportDataSourceState.swift | 28 + .../Camera/ViewportParametersProvider.swift | 100 ++ .../MapboxNavigationCore/Map/MapPoint.swift | 16 + .../MapboxNavigationCore/Map/MapView.swift | 205 +++ ...gationMapView+ContinuousAlternatives.swift | 64 + .../Map/NavigationMapView+Gestures.swift | 206 +++ ...NavigationMapView+VanishingRouteLine.swift | 212 +++ .../Map/NavigationMapView.swift | 736 +++++++++ .../Map/NavigationMapViewDelegate.swift | 289 ++++ .../Map/Other/Array.swift | 159 ++ .../Map/Other/CLLocationCoordinate2D++.swift | 28 + .../Map/Other/CongestionSegment.swift | 6 + .../Map/Other/Cosntants.swift | 22 + .../Map/Other/Expression++.swift | 61 + .../Map/Other/Feature++.swift | 28 + .../Map/Other/MapboxMap+Async.swift | 54 + .../Map/Other/NavigationMapIdentifiers.swift | 37 + .../Map/Other/PuckConfigurations.swift | 41 + .../Map/Other/RoadAlertType.swift | 121 ++ .../Map/Other/RoadClassesSegment.swift | 6 + .../Map/Other/Route.swift | 217 +++ .../RouteDurationAnnotationTailPosition.swift | 6 + .../Map/Other/RoutesPresentationStyle.swift | 14 + .../Map/Other/UIColor++.swift | 40 + .../Map/Other/UIFont.swift | 8 + .../Map/Other/UIImage++.swift | 47 + .../Map/Other/VectorSource++.swift | 84 + .../Style/AlternativeRoute+Deviation.swift | 29 + .../CongestionColorsConfiguration.swift | 76 + .../Congestion/CongestionConfiguration.swift | 24 + .../Map/Style/FeatureIds.swift | 169 ++ .../IntersectionAnnotationsMapFeatures.swift | 133 ++ .../Map/Style/ManeuverArrowMapFeatures.swift | 131 ++ .../Map/Style/MapFeatures/ETAView.swift | 285 ++++ .../ETAViewsAnnotationFeature.swift | 139 ++ .../Style/MapFeatures/GeoJsonMapFeature.swift | 239 +++ .../Map/Style/MapFeatures/MapFeature.swift | 16 + .../Style/MapFeatures/MapFeaturesStore.swift | 117 ++ .../Map/Style/MapFeatures/Style++.swift | 38 + .../Map/Style/MapLayersOrder.swift | 260 +++ .../Map/Style/NavigationMapStyleManager.swift | 537 +++++++ .../RouteAlertsAnnotationsMapFeatures.swift | 364 +++++ .../Style/RouteAnnotationMapFeatures.swift | 55 + .../Map/Style/RouteLineMapFeatures.swift | 406 +++++ .../Style/VoiceInstructionsMapFeatures.swift | 66 + .../Map/Style/WaypointsMapFeature.swift | 156 ++ .../ElectronicHorizonController.swift | 20 + .../MapboxNavigation/MapboxNavigation.swift | 47 + .../NavigationController.swift | 60 + .../MapboxNavigation/SessionController.swift | 44 + .../MapboxNavigationProvider.swift | 374 +++++ .../Navigator/AlternativeRoute.swift | 175 ++ .../Navigator/BorderCrossing.swift | 32 + .../EHorizon/DistancedRoadObject.swift | 155 ++ .../EHorizon/ElectronicHorizonConfig.swift | 57 + .../Navigator/EHorizon/Interchange.swift | 32 + .../Navigator/EHorizon/Junction.swift | 32 + .../EHorizon/LocalizedRoadObjectName.swift | 24 + .../Navigator/EHorizon/OpenLRIdentifier.swift | 32 + .../EHorizon/OpenLROrientation.swift | 38 + .../Navigator/EHorizon/OpenLRSideOfRoad.swift | 38 + .../Navigator/EHorizon/RoadGraph.swift | 70 + .../Navigator/EHorizon/RoadGraphEdge.swift | 73 + .../EHorizon/RoadGraphEdgeMetadata.swift | 195 +++ .../Navigator/EHorizon/RoadGraphPath.swift | 66 + .../EHorizon/RoadGraphPosition.swift | 42 + .../Navigator/EHorizon/RoadName.swift | 42 + .../Navigator/EHorizon/RoadObject.swift | 78 + .../EHorizon/RoadObjectEdgeLocation.swift | 38 + .../Navigator/EHorizon/RoadObjectKind.swift | 124 ++ .../EHorizon/RoadObjectLocation.swift | 127 ++ .../EHorizon/RoadObjectMatcher.swift | 215 +++ .../EHorizon/RoadObjectMatcherDelegate.swift | 29 + .../EHorizon/RoadObjectMatcherError.swift | 36 + .../EHorizon/RoadObjectPosition.swift | 29 + .../Navigator/EHorizon/RoadObjectStore.swift | 126 ++ .../EHorizon/RoadObjectStoreDelegate.swift | 21 + .../Navigator/EHorizon/RoadShield.swift | 42 + .../Navigator/EHorizon/RoadSubgraphEdge.swift | 65 + .../Navigator/EHorizon/RouteAlert.swift | 22 + .../Navigator/EtaDistanceInfo.swift | 12 + .../Navigator/FasterRouteController.swift | 152 ++ .../CoreNavigator/CoreNavigator.swift | 518 ++++++ .../DefaultRerouteControllerInterface.swift | 29 + .../NavigationNativeNavigator.swift | 143 ++ .../NavigationSessionManager.swift | 42 + .../NavigatorElectronicHorizonObserver.swift | 58 + .../NavigatorFallbackVersionsObserver.swift | 66 + .../NavigatorRouteAlternativesObserver.swift | 42 + .../NavigatorRouteRefreshObserver.swift | 67 + .../NavigatorStatusObserver.swift | 18 + .../CoreNavigator/RerouteController.swift | 151 ++ .../ReroutingControllerDelegate.swift | 51 + .../Navigator/Internals/HandlerFactory.swift | 87 + .../Movement/NavigationMovementMonitor.swift | 84 + .../Internals/NativeHandlersFactory.swift | 289 ++++ .../Internals/RoutesCoordinator.swift | 103 ++ .../Internals/Simulation/DispatchTimer.swift | 101 ++ .../Simulation/SimulatedLocationManager.swift | 493 ++++++ .../LocationClient/LocationClient.swift | 96 ++ .../LocationClient/LocationSource.swift | 8 + .../MultiplexLocationClient.swift | 139 ++ .../SimulatedLocationManagerWrapper.swift | 73 + .../Navigator/MapMatchingResult.swift | 79 + .../Navigator/MapboxNavigator.swift | 1414 +++++++++++++++++ .../Navigator/NavigationLocationManager.swift | 44 + .../Navigator/NavigationRoutes.swift | 420 +++++ .../Navigator/Navigator.swift | 403 +++++ .../Navigator/RoadInfo.swift | 40 + .../RouteProgress/RouteLegProgress.swift | 231 +++ .../RouteProgress/RouteProgress.swift | 419 +++++ .../RouteProgress/RouteStepProgress.swift | 277 ++++ .../Navigator/SpeedLimit.swift | 7 + .../Navigator/Tunnel.swift | 13 + .../PredictiveCacheConfig.swift | 29 + .../PredictiveCacheLocationConfig.swift | 39 + .../PredictiveCacheManager.swift | 111 ++ .../PredictiveCacheMapsConfig.swift | 31 + .../PredictiveCacheNavigationConfig.swift | 13 + .../PredictiveCacheSearchConfig.swift | 25 + .../MapboxNavigationCore/Resources/3DPuck.glb | Bin 0 -> 31480 bytes .../Resources/Assets.xcassets/Contents.json | 6 + .../RoadIntersections/Contents.json | 6 + .../RailroadCrossing.imageset/Contents.json | 12 + .../RailroadCrossing.pdf | Bin 0 -> 14532 bytes .../StopSign.imageset/Contents.json | 16 + .../StopSign.imageset/StopSign.pdf | Bin 0 -> 19197 bytes .../TrafficSignal.imageset/Contents.json | 16 + .../TrafficSignal.imageset/TrafficSignal.pdf | Bin 0 -> 10550 bytes .../YieldSign.imageset/Contents.json | 16 + .../YieldSign.imageset/YieldSign.pdf | Bin 0 -> 10639 bytes .../Assets.xcassets/RouteAlerts/Contents.json | 6 + .../ra_accident.imageset/Contents.json | 12 + .../ra_accident.imageset/ra_accident.pdf | Bin 0 -> 18167 bytes .../ra_congestion.imageset/Contents.json | 12 + .../ra_congestion.imageset/ra_congestion.pdf | Bin 0 -> 19663 bytes .../ra_construction.imageset/Contents.json | 12 + .../ra_contruction.pdf | Bin 0 -> 15309 bytes .../Contents.json | 12 + .../ra_disabled_vehicle.pdf | Bin 0 -> 16033 bytes .../Contents.json | 12 + .../ra_lane_restriction.pdf | Bin 0 -> 17449 bytes .../ra_mass_transit.imageset/Contents.json | 12 + .../ra_mass_transit.pdf | Bin 0 -> 15442 bytes .../ra_miscellaneous.imageset/Contents.json | 12 + .../ra_miscellaneous.pdf | Bin 0 -> 11988 bytes .../ra_other_news.imageset/Contents.json | 12 + .../ra_other_news.imageset/ra_other_news.pdf | Bin 0 -> 12005 bytes .../ra_planned_event.imageset/Contents.json | 12 + .../ra_planned_event.pdf | Bin 0 -> 13986 bytes .../ra_road_closure.imageset/Contents.json | 12 + .../ra_road_closure.pdf | Bin 0 -> 11476 bytes .../ra_road_hazard.imageset/Contents.json | 12 + .../ra_road_hazard.pdf | Bin 0 -> 12005 bytes .../ra_weather.imageset/Contents.json | 12 + .../ra_weather.imageset/ra_weather.pdf | Bin 0 -> 17306 bytes .../midpoint_marker.imageset/Contents.json | 12 + .../midpoint_marker.pdf | Bin 0 -> 21017 bytes .../puck.imageset/Contents.json | 12 + .../Assets.xcassets/puck.imageset/puck.pdf | Bin 0 -> 19977 bytes .../triangle.imageset/Contents.json | 15 + .../triangle.imageset/triangle.pdf | Bin 0 -> 3952 bytes .../Resources/Sounds/reroute-sound.pcm | Bin 0 -> 66284 bytes .../Resources/ar.lproj/Localizable.strings | 2 + .../Resources/bg.lproj/Localizable.strings | 2 + .../Resources/ca.lproj/Localizable.strings | 8 + .../Resources/cs.lproj/Localizable.strings | 2 + .../Resources/da.lproj/Localizable.strings | 2 + .../Resources/de.lproj/Localizable.strings | 8 + .../Resources/el.lproj/Localizable.strings | 2 + .../Resources/en.lproj/Localizable.strings | 8 + .../Resources/es.lproj/Localizable.strings | 8 + .../Resources/et.lproj/Localizable.strings | 2 + .../Resources/fi.lproj/Localizable.strings | 2 + .../Resources/fr.lproj/Localizable.strings | 2 + .../Resources/he.lproj/Localizable.strings | 2 + .../Resources/hr.lproj/Localizable.strings | 2 + .../Resources/hu.lproj/Localizable.strings | 2 + .../Resources/it.lproj/Localizable.strings | 2 + .../Resources/ja.lproj/Localizable.strings | 2 + .../Resources/ms.lproj/Localizable.strings | 2 + .../Resources/nl.lproj/Localizable.strings | 2 + .../Resources/no.lproj/Localizable.strings | 2 + .../Resources/pl.lproj/Localizable.strings | 8 + .../Resources/pt-BR.lproj/Localizable.strings | 2 + .../Resources/pt-PT.lproj/Localizable.strings | 2 + .../Resources/ro.lproj/Localizable.strings | 2 + .../Resources/ru.lproj/Localizable.strings | 8 + .../Resources/sk.lproj/Localizable.strings | 2 + .../Resources/sl.lproj/Localizable.strings | 2 + .../Resources/sr.lproj/Localizable.strings | 2 + .../Resources/sv.lproj/Localizable.strings | 2 + .../Resources/tr.lproj/Localizable.strings | 2 + .../Resources/uk.lproj/Localizable.strings | 8 + .../Resources/vi.lproj/Localizable.strings | 8 + .../zh-Hans.lproj/Localizable.strings | 2 + .../zh-Hant.lproj/Localizable.strings | 2 + .../Routing/MapboxRoutingProvider.swift | 253 +++ .../Routing/NavigationRouteOptions.swift | 227 +++ .../Routing/RoutingProvider.swift | 45 + .../MapboxNavigationCore/SdkInfo.swift | 23 + .../AlternativeRoutesDetectionConfig.swift | 103 ++ .../Settings/BillingHandlerProvider.swift | 16 + .../Settings/Configuration/CoreConfig.swift | 222 +++ .../Configuration/UnitOfMeasurement.swift | 11 + .../Settings/CustomRoutingProvider.swift | 16 + .../Settings/EventsManagerProvider.swift | 16 + .../Settings/FasterRouteDetectionConfig.swift | 44 + .../Settings/HistoryRecordingConfig.swift | 16 + .../Settings/IncidentsConfig.swift | 26 + .../NavigationCoreApiConfiguration.swift | 41 + .../Settings/RerouteConfig.swift | 26 + .../Settings/RoutingConfig.swift | 87 + .../Settings/SettingsWrappers.swift | 40 + .../Settings/StatusUpdatingSettings.swift | 22 + .../Settings/TTSConfig.swift | 19 + .../Settings/TelemetryAppMetadata.swift | 48 + .../Settings/TileStoreConfiguration.swift | 65 + .../Telemetry/ConnectivityTypeProvider.swift | 75 + .../Telemetry/EventAppState.swift | 153 ++ .../Telemetry/EventsMetadataProvider.swift | 176 ++ .../NavigationNativeEventsManager.swift | 174 ++ .../NavigationTelemetryManager.swift | 42 + .../MapboxNavigationCore/Typealiases.swift | 32 + .../Utils/NavigationLog.swift | 58 + .../Utils/ScreenCapture.swift | 47 + .../Utils/UnimplementedLogging.swift | 98 ++ .../Utils/UserAgent.swift | 100 ++ .../MapboxNavigationCore/Version.swift | 6 + .../VoiceGuidance/AudioPlayerClient.swift | 93 ++ .../VoiceGuidance/AudioPlayerDelegate.swift | 11 + .../MapboxSpeechSynthesizer.swift | 381 +++++ .../MultiplexedSpeechSynthesizer.swift | 181 +++ .../VoiceGuidance/RouteVoiceController.swift | 150 ++ .../VoiceGuidance/Speech.swift | 201 +++ .../VoiceGuidance/SpeechError.swift | 60 + .../VoiceGuidance/SpeechOptions.swift | 82 + .../VoiceGuidance/SpeechSynthesizing.swift | 91 ++ .../SystemSpeechSynthesizer.swift | 251 +++ scripts/checkout-navigation.sh | 9 + 404 files changed, 38057 insertions(+) create mode 100644 ios/Classes/Navigation/.gitkeep create mode 100644 ios/Classes/Navigation/MapboxDirections/AdministrativeRegion.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Amenity.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/AmenityType.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/AttributeOptions.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/BlockedLanes.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Congestion.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Credentials.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/CustomValueOptionSet.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Directions.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/DirectionsError.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/DirectionsOptions.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/DirectionsResult.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/DrivingSide.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/Array.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/Codable.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/CoreLocation.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/Double.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/ForeignMemberContainer.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/GeoJSON.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/HTTPURLResponse.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/Measurement.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/String.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/URL+Request.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Incident.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Interchange.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Intersection.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/IsochroneError.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/IsochroneOptions.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Isochrones.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Junction.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Lane.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/LaneIndication.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/MapMatching/MapMatchingResponse.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/MapMatching/Match.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/MapMatching/MatchOptions.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/MapMatching/Tracepoint.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/MapboxDirections.h create mode 100644 ios/Classes/Navigation/MapboxDirections/MapboxStreetsRoadClass.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Matrix.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/MatrixError.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/MatrixOptions.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/MatrixResponse.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/OfflineDirections.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Polyline.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/ProfileIdentifier.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/QuickLook.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RefreshedRoute.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/ResponseDisposition.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RestStop.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RoadClassExclusionViolation.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RoadClasses.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Route.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RouteLeg.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RouteLegAttributes.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RouteOptions.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RouteRefreshResponse.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RouteRefreshSource.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RouteResponse.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/RouteStep.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/SilentWaypoint.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/SpokenInstruction.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/TollCollection.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/TollPrice.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/TrafficTendency.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/VisualInstruction.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/VisualInstructionBanner.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/VisualInstructionComponent.swift create mode 100644 ios/Classes/Navigation/MapboxDirections/Waypoint.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Billing/ApiConfiguration.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler+SkuTokenProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Billing/SkuTokenProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Cache/FileCache.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Cache/SyncBimodalCache.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/CoreConstants.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Environment.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/AVAudioSession.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/AmenityType.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Array++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/BoundingBox++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Bundle.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/CLLocationDirection++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/CongestionLevel.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Coordinate2D.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Date.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Dictionary.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/FixLocation.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Geometry.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Incident.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Locale.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/MapboxStreetsRoadClass.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/MeasurementSystem.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/NavigationStatus.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Preconcurrency+Sendable.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/RestStop.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Result.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteLeg.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteOptions.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/SpokenInstruction.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/String.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/TollCollection.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIDevice.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIEdgeInsets.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Utils.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/ActiveNavigationFeedbackType.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventFixLocation.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventStep.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventsManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackEvent.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackMetadata.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackScreenshotOption.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackType.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/NavigationEventsManagerError.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/PassiveNavigationFeedbackType.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/SearchFeedbackType.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/AttachmentsUploader.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/CopilotService.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/ApplicationState.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/DriveEnds.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/InitRoute.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationFeedback.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationHistoryEvent.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResultUsed.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResults.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/FeedbackEventsObserver.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilot.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilotDelegate.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryAttachmentProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryErrorReport.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryEventsController.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryFormat.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryLocalStorage.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryUploader.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationSession.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/AppEnvironment.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/FileManager++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TokenOwnerProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TypeConverter.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/HistoryEvent.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReader.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecorder.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecording.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReplayer.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/IdleTimerManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Localization/LocalizationManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Localization/String+Localization.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/CameraStateTransition.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/FollowingCameraOptions.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCamera.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraDebugView.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraOptions.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraState.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraStateTransition.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraType.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationViewportDataSourceOptions.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/OverviewCameraOptions.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CarPlayViewportDataSource.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CommonViewportDataSource.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/MobileViewportDataSource.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource+Calculation.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSourceState.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportParametersProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/MapPoint.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/MapView.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+ContinuousAlternatives.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+Gestures.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+VanishingRouteLine.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapViewDelegate.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Array.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/CLLocationCoordinate2D++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/CongestionSegment.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Cosntants.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Expression++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Feature++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/MapboxMap+Async.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/PuckConfigurations.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadAlertType.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadClassesSegment.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Route.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RouteDurationAnnotationTailPosition.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoutesPresentationStyle.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIColor++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIFont.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIImage++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/VectorSource++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionColorsConfiguration.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionConfiguration.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/FeatureIds.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapLayersOrder.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/ElectronicHorizonController.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/MapboxNavigation.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/NavigationController.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/SessionController.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigationProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/AlternativeRoute.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/BorderCrossing.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/DistancedRoadObject.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/ElectronicHorizonConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Interchange.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Junction.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/LocalizedRoadObjectName.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRIdentifier.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLROrientation.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRSideOfRoad.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraph.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdge.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdgeMetadata.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPath.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPosition.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadName.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObject.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectEdgeLocation.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectKind.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectLocation.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcher.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherDelegate.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherError.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectPosition.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStore.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStoreDelegate.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadShield.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadSubgraphEdge.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RouteAlert.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EtaDistanceInfo.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/FasterRouteController.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/CoreNavigator.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/DefaultRerouteControllerInterface.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationNativeNavigator.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationSessionManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorElectronicHorizonObserver.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorFallbackVersionsObserver.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteAlternativesObserver.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteRefreshObserver.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorStatusObserver.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/RerouteController.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/ReroutingControllerDelegate.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/HandlerFactory.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Movement/NavigationMovementMonitor.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/NativeHandlersFactory.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/RoutesCoordinator.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/DispatchTimer.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/SimulatedLocationManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationClient.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationSource.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/MultiplexLocationClient.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/SimulatedLocationManagerWrapper.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapMatchingResult.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapboxNavigator.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationLocationManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationRoutes.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Navigator.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/RoadInfo.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteLegProgress.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteProgress.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteStepProgress.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/SpeedLimit.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Tunnel.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheLocationConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheMapsConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheNavigationConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheSearchConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/3DPuck.glb create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/RailroadCrossing.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/StopSign.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/StopSign.imageset/StopSign.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/TrafficSignal.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/TrafficSignal.imageset/TrafficSignal.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/YieldSign.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/ra_accident.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_congestion.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_congestion.imageset/ra_congestion.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/ra_contruction.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/ra_disabled_vehicle.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/ra_lane_restriction.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/ra_mass_transit.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/ra_miscellaneous.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/ra_other_news.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/ra_planned_event.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/ra_road_closure.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_hazard.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_hazard.imageset/ra_road_hazard.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/ra_weather.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/midpoint_marker.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/puck.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/triangle.imageset/Contents.json create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/triangle.imageset/triangle.pdf create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Sounds/reroute-sound.pcm create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ar.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/bg.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ca.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/cs.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/da.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/de.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/el.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/en.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/es.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/et.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/fi.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/fr.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/he.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/hr.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/hu.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/it.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ja.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ms.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/nl.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/no.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/pl.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-BR.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-PT.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ro.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ru.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/sk.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/sl.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/sr.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/sv.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/tr.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/uk.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/vi.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hans.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hant.lproj/Localizable.strings create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Routing/MapboxRoutingProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Routing/NavigationRouteOptions.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Routing/RoutingProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/SdkInfo.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/AlternativeRoutesDetectionConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/BillingHandlerProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/CoreConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/UnitOfMeasurement.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/CustomRoutingProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/EventsManagerProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/FasterRouteDetectionConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/HistoryRecordingConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/IncidentsConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/NavigationCoreApiConfiguration.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/RerouteConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/RoutingConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/SettingsWrappers.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/StatusUpdatingSettings.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/TTSConfig.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/TelemetryAppMetadata.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/TileStoreConfiguration.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Telemetry/ConnectivityTypeProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventAppState.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventsMetadataProvider.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationNativeEventsManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationTelemetryManager.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Typealiases.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Utils/NavigationLog.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Utils/ScreenCapture.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Utils/UnimplementedLogging.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Utils/UserAgent.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Version.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerClient.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerDelegate.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MapboxSpeechSynthesizer.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MultiplexedSpeechSynthesizer.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/RouteVoiceController.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/Speech.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechError.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechOptions.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechSynthesizing.swift create mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SystemSpeechSynthesizer.swift create mode 100644 scripts/checkout-navigation.sh diff --git a/ios/Classes/Navigation/.gitkeep b/ios/Classes/Navigation/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ios/Classes/Navigation/MapboxDirections/AdministrativeRegion.swift b/ios/Classes/Navigation/MapboxDirections/AdministrativeRegion.swift new file mode 100644 index 000000000..6938ae717 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/AdministrativeRegion.swift @@ -0,0 +1,43 @@ +import Foundation +import Turf + +/// ``AdministrativeRegion`` describes corresponding object on the route. +/// +/// You can also use ``Intersection/regionCode`` or ``RouteLeg/regionCode(atStepIndex:intersectionIndex:)`` to +/// retrieve ISO 3166-1 country code. +public struct AdministrativeRegion: Codable, Equatable, ForeignMemberContainer, Sendable { + public var foreignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey { + case countryCodeAlpha3 = "iso_3166_1_alpha3" + case countryCode = "iso_3166_1" + } + + /// ISO 3166-1 alpha-3 country code + public var countryCodeAlpha3: String? + /// ISO 3166-1 country code + public var countryCode: String + + public init(countryCode: String, countryCodeAlpha3: String) { + self.countryCode = countryCode + self.countryCodeAlpha3 = countryCodeAlpha3 + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.countryCode = try container.decode(String.self, forKey: .countryCode) + self.countryCodeAlpha3 = try container.decodeIfPresent(String.self, forKey: .countryCodeAlpha3) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(countryCode, forKey: .countryCode) + try container.encodeIfPresent(countryCodeAlpha3, forKey: .countryCodeAlpha3) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Amenity.swift b/ios/Classes/Navigation/MapboxDirections/Amenity.swift new file mode 100644 index 000000000..37a4ffb6a --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Amenity.swift @@ -0,0 +1,50 @@ +import Foundation + +/// Provides information about amenity that is available at a given ``RestStop``. +public struct Amenity: Codable, Equatable, Sendable { + /// Name of the amenity, if available. + public let name: String? + + /// Brand of the amenity, if available. + public let brand: String? + + /// Type of the amenity. + public let type: AmenityType + + private enum CodingKeys: String, CodingKey { + case type + case name + case brand + } + + /// Initializes an ``Amenity``. + /// - Parameters: + /// - type: Type of the amenity. + /// - name: Name of the amenity. + /// - brand: Brand of the amenity. + public init(type: AmenityType, name: String? = nil, brand: String? = nil) { + self.type = type + self.name = name + self.brand = brand + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.type = try container.decode(AmenityType.self, forKey: .type) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + self.brand = try container.decodeIfPresent(String.self, forKey: .brand) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + try container.encodeIfPresent(name, forKey: .name) + try container.encodeIfPresent(brand, forKey: .brand) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.name == rhs.name && + lhs.brand == rhs.brand && + lhs.type == rhs.type + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/AmenityType.swift b/ios/Classes/Navigation/MapboxDirections/AmenityType.swift new file mode 100644 index 000000000..73889ccf7 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/AmenityType.swift @@ -0,0 +1,61 @@ +import Foundation + +/// Type of the ``Amenity``. +public enum AmenityType: String, Codable, Equatable, Sendable { + /// Undefined amenity type. + case undefined + + /// Gas station amenity type. + case gasStation = "gas_station" + + /// Electric charging station amenity type. + case electricChargingStation = "electric_charging_station" + + /// Toilet amenity type. + case toilet + + /// Coffee amenity type. + case coffee + + /// Restaurant amenity type. + case restaurant + + /// Snack amenity type. + case snack + + /// ATM amenity type. + case ATM + + /// Info amenity type. + case info + + /// Baby care amenity type. + case babyCare = "baby_care" + + /// Facilities for disabled amenity type. + case facilitiesForDisabled = "facilities_for_disabled" + + /// Shop amenity type. + case shop + + /// Telephone amenity type. + case telephone + + /// Hotel amenity type. + case hotel + + /// Hot spring amenity type. + case hotSpring = "hotspring" + + /// Shower amenity type. + case shower + + /// Picnic shelter amenity type. + case picnicShelter = "picnic_shelter" + + /// Post amenity type. + case post + + /// Fax amenity type. + case fax +} diff --git a/ios/Classes/Navigation/MapboxDirections/AttributeOptions.swift b/ios/Classes/Navigation/MapboxDirections/AttributeOptions.swift new file mode 100644 index 000000000..efab61cf0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/AttributeOptions.swift @@ -0,0 +1,154 @@ +import Foundation + +/// Attributes are metadata information for a route leg. +/// +/// When any of the attributes are specified, the resulting route leg contains one attribute value for each segment in +/// leg, where a segment is the straight line between two coordinates in the route leg’s full geometry. +public struct AttributeOptions: CustomValueOptionSet, CustomStringConvertible, Equatable, Sendable { + public var rawValue: Int + + public var customOptionsByRawValue: [Int: String] = [:] + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + /// Live-traffic closures along the road segment. + /// + /// When this attribute is specified, the ``RouteLeg/closures`` property is filled with relevant data. + /// + /// This attribute requires ``ProfileIdentifier/automobileAvoidingTraffic`` and is supported only by Directions and + /// Map Matching requests. + public static let closures = AttributeOptions(rawValue: 1) + + /// Distance (in meters) along the segment. + /// + /// When this attribute is specified, the ``RouteLeg/segmentDistances`` property contains one value for each segment + /// in the leg’s full geometry. + /// When used in Matrix request - will produce a distances matrix in response. + public static let distance = AttributeOptions(rawValue: 1 << 1) + + /// Expected travel time (in seconds) along the segment. + /// + /// When this attribute is specified, the ``RouteLeg/expectedSegmentTravelTimes`` property contains one value for + /// each segment in the leg’s full geometry. + /// When used in Matrix request - will produce a durations matrix in response. + public static let expectedTravelTime = AttributeOptions(rawValue: 1 << 2) + + /// Current average speed (in meters per second) along the segment. + /// + /// When this attribute is specified, the ``RouteLeg/segmentSpeeds`` property contains one value for each segment in + /// the leg’s full geometry. This attribute is supported only by Directions and Map Matching requests. + public static let speed = AttributeOptions(rawValue: 1 << 3) + + /// Traffic congestion level along the segment. + /// + /// When this attribute is specified, the ``RouteLeg/segmentCongestionLevels`` property contains one value for each + /// segment + /// in the leg’s full geometry. + /// + /// This attribute requires ``ProfileIdentifier/automobileAvoidingTraffic`` and is supported only by Directions and + /// Map Matching requests. Any other profile identifier produces ``CongestionLevel/unknown`` for each segment along + /// the route. + public static let congestionLevel = AttributeOptions(rawValue: 1 << 4) + + /// The maximum speed limit along the segment. + /// + /// When this attribute is specified, the ``RouteLeg/segmentMaximumSpeedLimits`` property contains one value for + /// each segment in the leg’s full geometry. This attribute is supported only by Directions and Map Matching + /// requests. + public static let maximumSpeedLimit = AttributeOptions(rawValue: 1 << 5) + + /// Traffic congestion level in numeric form. + /// + /// When this attribute is specified, the ``RouteLeg/segmentNumericCongestionLevels`` property contains one value + /// for each + /// segment in the leg’s full geometry. + /// This attribute requires ``ProfileIdentifier/automobileAvoidingTraffic`` and is supported only by Directions and + /// Map Matching requests. Any other profile identifier produces `nil` for each segment along the route. + public static let numericCongestionLevel = AttributeOptions(rawValue: 1 << 6) + + /// The tendency value conveys the changing state of traffic congestion (increasing, decreasing, constant etc). + public static let trafficTendency = AttributeOptions(rawValue: 1 << 7) + + /// Creates an ``AttributeOptions`` from the given description strings. + public init?(descriptions: [String]) { + var attributeOptions: AttributeOptions = [] + for description in descriptions { + switch description { + case "closure": + attributeOptions.update(with: .closures) + case "distance": + attributeOptions.update(with: .distance) + case "duration": + attributeOptions.update(with: .expectedTravelTime) + case "speed": + attributeOptions.update(with: .speed) + case "congestion": + attributeOptions.update(with: .congestionLevel) + case "maxspeed": + attributeOptions.update(with: .maximumSpeedLimit) + case "congestion_numeric": + attributeOptions.update(with: .numericCongestionLevel) + case "traffic_tendency": + attributeOptions.update(with: .trafficTendency) + case "": + continue + default: + return nil + } + } + self.init(rawValue: attributeOptions.rawValue) + } + + public var description: String { + var descriptions: [String] = [] + if contains(.closures) { + descriptions.append("closure") + } + if contains(.distance) { + descriptions.append("distance") + } + if contains(.expectedTravelTime) { + descriptions.append("duration") + } + if contains(.speed) { + descriptions.append("speed") + } + if contains(.congestionLevel) { + descriptions.append("congestion") + } + if contains(.maximumSpeedLimit) { + descriptions.append("maxspeed") + } + if contains(.numericCongestionLevel) { + descriptions.append("congestion_numeric") + } + if contains(.trafficTendency) { + descriptions.append("traffic_tendency") + } + for (key, value) in customOptionsByRawValue { + if rawValue & key != 0 { + descriptions.append(value) + } + } + return descriptions.joined(separator: ",") + } +} + +extension AttributeOptions: Codable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description.components(separatedBy: ",").filter { !$0.isEmpty }) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let descriptions = try container.decode([String].self) + self = AttributeOptions(descriptions: descriptions)! + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/BlockedLanes.swift b/ios/Classes/Navigation/MapboxDirections/BlockedLanes.swift new file mode 100644 index 000000000..cb6f49b9e --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/BlockedLanes.swift @@ -0,0 +1,134 @@ + +import Foundation + +/// Defines a lane affected by the ``Incident`` +public struct BlockedLanes: OptionSet, CustomStringConvertible, Equatable, Sendable { + public var rawValue: Int + var stringKey: String? + + public init(rawValue: Int) { + self.init(rawValue: rawValue, key: nil) + } + + init(rawValue: Int, key: String?) { + self.rawValue = rawValue + self.stringKey = key + } + + /// Left lane + public static let left = BlockedLanes(rawValue: 1 << 0, key: "LEFT") + /// Left center lane + /// + /// Usually refers to the second lane from left on a four-lane highway + public static let leftCenter = BlockedLanes(rawValue: 1 << 1, key: "LEFT CENTER") + /// Left turn lane + public static let leftTurnLane = BlockedLanes(rawValue: 1 << 2, key: "LEFT TURN LANE") + /// Center lane + public static let center = BlockedLanes(rawValue: 1 << 3, key: "CENTER") + /// Right lane + public static let right = BlockedLanes(rawValue: 1 << 4, key: "RIGHT") + /// Right center lane + /// + /// Usually refers to the second lane from right on a four-lane highway + public static let rightCenter = BlockedLanes(rawValue: 1 << 5, key: "RIGHT CENTER") + /// Right turn lane + public static let rightTurnLane = BlockedLanes(rawValue: 1 << 6, key: "RIGHT TURN LANE") + /// High occupancy vehicle lane + public static let highOccupancyVehicle = BlockedLanes(rawValue: 1 << 7, key: "HOV") + /// Side lane + public static let side = BlockedLanes(rawValue: 1 << 8, key: "SIDE") + /// Shoulder lane + public static let shoulder = BlockedLanes(rawValue: 1 << 9, key: "SHOULDER") + /// Median lane + public static let median = BlockedLanes(rawValue: 1 << 10, key: "MEDIAN") + /// 1st Lane. + public static let lane1 = BlockedLanes(rawValue: 1 << 11, key: "1") + /// 2nd Lane. + public static let lane2 = BlockedLanes(rawValue: 1 << 12, key: "2") + /// 3rd Lane. + public static let lane3 = BlockedLanes(rawValue: 1 << 13, key: "3") + /// 4th Lane. + public static let lane4 = BlockedLanes(rawValue: 1 << 14, key: "4") + /// 5th Lane. + public static let lane5 = BlockedLanes(rawValue: 1 << 15, key: "5") + /// 6th Lane. + public static let lane6 = BlockedLanes(rawValue: 1 << 16, key: "6") + /// 7th Lane. + public static let lane7 = BlockedLanes(rawValue: 1 << 17, key: "7") + /// 8th Lane. + public static let lane8 = BlockedLanes(rawValue: 1 << 18, key: "8") + /// 9th Lane. + public static let lane9 = BlockedLanes(rawValue: 1 << 19, key: "9") + /// 10th Lane. + public static let lane10 = BlockedLanes(rawValue: 1 << 20, key: "10") + + static var allLanes: [BlockedLanes] { + return [ + .left, + .leftCenter, + .leftTurnLane, + .center, + .right, + .rightCenter, + .rightTurnLane, + .highOccupancyVehicle, + .side, + .shoulder, + .median, + .lane1, + .lane2, + .lane3, + .lane4, + .lane5, + .lane6, + .lane7, + .lane8, + .lane9, + .lane10, + ] + } + + /// Creates a ``BlockedLanes`` given an array of strings. + /// + /// Resulting options set will only contain known values. If string description does not match any known `Blocked + /// Lane` identifier - it will be ignored. + public init?(descriptions: [String]) { + var blockedLanes: BlockedLanes = [] + Self.allLanes.forEach { + if descriptions.contains($0.stringKey!) { + blockedLanes.insert($0) + } + } + self.init(rawValue: blockedLanes.rawValue) + } + + /// String representation of ``BlockedLanes`` options set. + /// + /// Resulting description contains only texts for known options. Custom options will be ignored if any. + public var description: String { + var descriptions: [String] = [] + Self.allLanes.forEach { + if contains($0) { + descriptions.append($0.stringKey!) + } + } + return descriptions.joined(separator: ",") + } +} + +extension BlockedLanes: Codable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description.components(separatedBy: ",").filter { !$0.isEmpty }) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let descriptions = try container.decode([String].self) + if let roadClasses = BlockedLanes(descriptions: descriptions) { + self = roadClasses + } else { + throw DirectionsError.invalidResponse(nil) + } + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Congestion.swift b/ios/Classes/Navigation/MapboxDirections/Congestion.swift new file mode 100644 index 000000000..867b44d24 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Congestion.swift @@ -0,0 +1,33 @@ +import Foundation + +/// A ``CongestionLevel`` indicates the level of traffic congestion along a road segment relative to the normal flow of +/// traffic along that segment. You can color-code a route line according to the congestion level along each segment of +/// the route. +public enum CongestionLevel: String, Codable, CaseIterable, Equatable, Sendable { + /// There is not enough data to determine the level of congestion along the road segment. + case unknown + + /// The road segment has little or no congestion. Traffic is flowing smoothly. + /// + /// Low congestion levels are conventionally highlighted in green or not highlighted at all. + case low + + /// The road segment has moderate, stop-and-go congestion. Traffic is flowing but speed is impeded. + /// + /// Moderate congestion levels are conventionally highlighted in yellow. + case moderate + + /// The road segment has heavy, bumper-to-bumper congestion. Traffic is barely moving. + /// + /// Heavy congestion levels are conventionally highlighted in orange. + case heavy + + /// The road segment has severe congestion. Traffic may be completely stopped. + /// + /// Severe congestion levels are conventionally highlighted in red. + case severe +} + +/// `NumericCongestionLevel` is the level of traffic congestion along a road segment in numeric form, from 0-100. A +/// value of 0 indicates no congestion, a value of 100 indicates maximum congestion. +public typealias NumericCongestionLevel = Int diff --git a/ios/Classes/Navigation/MapboxDirections/Credentials.swift b/ios/Classes/Navigation/MapboxDirections/Credentials.swift new file mode 100644 index 000000000..f220d01af --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Credentials.swift @@ -0,0 +1,80 @@ +import Foundation + +/// The Mapbox access token specified in the main application bundle’s Info.plist. +let defaultAccessToken: String? = + Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String ?? + Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAccessToken") as? String ?? + UserDefaults.standard.string(forKey: "MBXAccessToken") +let defaultApiEndPointURLString = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAPIBaseURL") as? String + +public struct Credentials: Equatable, Sendable { + /// The mapbox access token. You can find this in your Mapbox account dashboard. + public let accessToken: String? + + /// The host to reach. defaults to `api.mapbox.com`. + public let host: URL + + /// The SKU Token associated with the request. Used for billing. + public var skuToken: String? { +#if !os(Linux) + guard let mbx: AnyClass = NSClassFromString("MBXAccounts"), + mbx.responds(to: Selector(("serviceSkuToken"))), + let serviceSkuToken = mbx.value(forKeyPath: "serviceSkuToken") as? String + else { return nil } + + if mbx.responds(to: Selector(("serviceAccessToken"))) { + guard let serviceAccessToken = mbx.value(forKeyPath: "serviceAccessToken") as? String, + serviceAccessToken == accessToken + else { return nil } + + return serviceSkuToken + } else { + return serviceSkuToken + } +#else + return nil +#endif + } + + /// Intialize a new credential. + /// - Parameters: + /// - token: An access token to provide. If this value is nil, the SDK will attempt to find a token from your + /// app's `info.plist`. + /// - host: An optional parameter to pass a custom host. If `nil` is provided, the SDK will attempt to find a host + /// from your app's `info.plist`, and barring that will default to `https://api.mapbox.com`. + public init(accessToken token: String? = nil, host: URL? = nil) { + let accessToken = token ?? defaultAccessToken + + precondition( + accessToken != nil && !accessToken!.isEmpty, + "A Mapbox access token is required. Go to . In Info.plist, set the MBXAccessToken key to your access token, or use the Directions(accessToken:host:) initializer." + ) + self.accessToken = accessToken + if let host { + self.host = host + } else if let defaultHostString = defaultApiEndPointURLString, + let defaultHost = URL(string: defaultHostString) + { + self.host = defaultHost + } else { + self.host = URL(string: "https://api.mapbox.com")! + } + } + + /// Attempts to get ``host`` and ``accessToken`` from provided URL to create ``Credentials`` instance. + /// + /// If it is impossible to extract parameter(s) - default values will be used. + public init(requestURL url: URL) { + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + let accessToken = components? + .queryItems? + .first { $0.name == "access_token" }? + .value + components?.path = "/" + components?.queryItems = nil + self.init(accessToken: accessToken, host: components?.url) + } +} + +@available(*, deprecated, renamed: "Credentials") +public typealias DirectionsCredentials = Credentials diff --git a/ios/Classes/Navigation/MapboxDirections/CustomValueOptionSet.swift b/ios/Classes/Navigation/MapboxDirections/CustomValueOptionSet.swift new file mode 100644 index 000000000..75b4e5dd8 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/CustomValueOptionSet.swift @@ -0,0 +1,675 @@ +import Foundation + +/// Describes how ``CustomValueOptionSet/customOptionsByRawValue`` component is compared during logical operations in +/// ``CustomValueOptionSet``. +public enum CustomOptionComparisonPolicy: Equatable, Sendable { + /// Custom options are equal if ``CustomValueOptionSet/customOptionsByRawValue`` key-value pairs are strictly equal + /// + /// Example: + /// [1: "value1"] == [1: "value1"] + /// [1: "value1"] != [1: "value2"] + /// [1: "value1"] != [:] + /// [:] == [:] + case equal + /// Custom options are equal if ``CustomValueOptionSet/customOptionsByRawValue`` by the given key is equal or `nil` + /// + /// Example: + /// [1: "value1"] == [1: "value1"] + /// [1: "value1"] != [1: "value2"] + /// [1: "value1"] == [:] + /// [:] == [:] + case equalOrNull + /// Custom options are not compared. Only `rawValue` is taken into account when comparing ``CustomValueOptionSet``s. + /// + /// Example: + /// [1: "value1"] == [1: "value1"] + /// [1: "value1"] == [1: "value2"] + /// [1: "value1"] == [:] + /// [:] == [:] + case rawValueEqual +} + +/// Option set implementation which allows each option to have custom string value attached. +public protocol CustomValueOptionSet: OptionSet where RawValue: FixedWidthInteger, Element == Self { + associatedtype Element = Self + associatedtype CustomValue: Equatable + var rawValue: Self.RawValue { get set } + + /// Provides a text value description for user-provided options. + /// + /// The option set will recognize a custom option if it's unique `rawValue` flag is set and + /// ``customOptionsByRawValue`` contains a description for that flag. + /// Use the ``update(customOption:comparisonPolicy:)`` method to append a custom option. + var customOptionsByRawValue: [RawValue: CustomValue] { get set } + + init(rawValue: Self.RawValue) + + /// Returns a Boolean value that indicates whether the given element exists + /// in the set. + /// + /// This example uses the `contains(_:)` method to test whether an integer is + /// a member of a set of prime numbers. + /// + /// let primes: Set = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37] + /// let x = 5 + /// if primes.contains(x) { + /// print("\(x) is prime!") + /// } else { + /// print("\(x). Not prime.") + /// } + /// // Prints "5 is prime!" + /// + /// - Parameter member: An element to look for in the set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: `true` if `member` exists in the set; otherwise, `false`. + func contains(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool + /// Returns a new set with the elements of both this and the given set. + /// + /// In the following example, the `attendeesAndVisitors` set is made up + /// of the elements of the `attendees` and `visitors` sets: + /// + /// let attendees: Set = ["Alicia", "Bethany", "Diana"] + /// let visitors = ["Marcia", "Nathaniel"] + /// let attendeesAndVisitors = attendees.union(visitors) + /// print(attendeesAndVisitors) + /// // Prints "["Diana", "Nathaniel", "Bethany", "Alicia", "Marcia"]" + /// + /// If the set already contains one or more elements that are also in + /// `other`, the existing members are kept. + /// + /// let initialIndices = Set(0..<5) + /// let expandedIndices = initialIndices.union([2, 3, 6, 7]) + /// print(expandedIndices) + /// // Prints "[2, 4, 6, 7, 0, 1, 3]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: A new set with the unique elements of this set and `other`. + /// + /// - Note: if this set and `other` contain elements that are equal but + /// distinguishable (e.g. via `===`), which of these elements is present + /// in the result is unspecified. + func union(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element + /// Adds the elements of the given set to the set. + /// + /// In the following example, the elements of the `visitors` set are added to + /// the `attendees` set: + /// + /// var attendees: Set = ["Alicia", "Bethany", "Diana"] + /// let visitors: Set = ["Diana", "Marcia", "Nathaniel"] + /// attendees.formUnion(visitors) + /// print(attendees) + /// // Prints "["Diana", "Nathaniel", "Bethany", "Alicia", "Marcia"]" + /// + /// If the set already contains one or more elements that are also in + /// `other`, the existing members are kept. + /// + /// var initialIndices = Set(0..<5) + /// initialIndices.formUnion([2, 3, 6, 7]) + /// print(initialIndices) + /// // Prints "[2, 4, 6, 7, 0, 1, 3]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + mutating func formUnion(_ other: Self, comparisonPolicy: CustomOptionComparisonPolicy) + /// Returns a new set with the elements that are common to both this set and + /// the given set. + /// + /// In the following example, the `bothNeighborsAndEmployees` set is made up + /// of the elements that are in *both* the `employees` and `neighbors` sets. + /// Elements that are in only one or the other are left out of the result of + /// the intersection. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] + /// let bothNeighborsAndEmployees = employees.intersection(neighbors) + /// print(bothNeighborsAndEmployees) + /// // Prints "["Bethany", "Eric"]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: A new set. + /// + /// - Note: if this set and `other` contain elements that are equal but + /// distinguishable (e.g. via `===`), which of these elements is present + /// in the result is unspecified. + func intersection(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element + /// Removes the elements of this set that aren't also in the given set. + /// + /// In the following example, the elements of the `employees` set that are + /// not also members of the `neighbors` set are removed. In particular, the + /// names `"Alicia"`, `"Chris"`, and `"Diana"` are removed. + /// + /// var employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] + /// employees.formIntersection(neighbors) + /// print(employees) + /// // Prints "["Bethany", "Eric"]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + mutating func formIntersection(_ other: Self, comparisonPolicy: CustomOptionComparisonPolicy) + /// Returns a new set with the elements that are either in this set or in the + /// given set, but not in both. + /// + /// In the following example, the `eitherNeighborsOrEmployees` set is made up + /// of the elements of the `employees` and `neighbors` sets that are not in + /// both `employees` *and* `neighbors`. In particular, the names `"Bethany"` + /// and `"Eric"` do not appear in `eitherNeighborsOrEmployees`. + /// + /// let employees: Set = ["Alicia", "Bethany", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani"] + /// let eitherNeighborsOrEmployees = employees.symmetricDifference(neighbors) + /// print(eitherNeighborsOrEmployees) + /// // Prints "["Diana", "Forlani", "Alicia"]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: A new set. + func symmetricDifference(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element + /// Removes the elements of the set that are also in the given set and adds + /// the members of the given set that are not already in the set. + /// + /// In the following example, the elements of the `employees` set that are + /// also members of `neighbors` are removed from `employees`, while the + /// elements of `neighbors` that are not members of `employees` are added to + /// `employees`. In particular, the names `"Bethany"` and `"Eric"` are + /// removed from `employees` while the name `"Forlani"` is added. + /// + /// var employees: Set = ["Alicia", "Bethany", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani"] + /// employees.formSymmetricDifference(neighbors) + /// print(employees) + /// // Prints "["Diana", "Forlani", "Alicia"]" + /// + /// - Parameter other: A set of the same type. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + mutating func formSymmetricDifference(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) + /// Returns a new set containing the elements of this set that do not occur + /// in the given set. + /// + /// In the following example, the `nonNeighbors` set is made up of the + /// elements of the `employees` set that are not elements of `neighbors`: + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] + /// let nonNeighbors = employees.subtracting(neighbors) + /// print(nonNeighbors) + /// // Prints "["Diana", "Chris", "Alicia"]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: A new set. + func subtracting(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element + /// Removes the elements of the given set from this set. + /// + /// In the following example, the elements of the `employees` set that are + /// also members of the `neighbors` set are removed. In particular, the + /// names `"Bethany"` and `"Eric"` are removed from `employees`. + /// + /// var employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] + /// employees.subtract(neighbors) + /// print(employees) + /// // Prints "["Diana", "Chris", "Alicia"]" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + mutating func subtract(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) + /// Inserts the given element in the set if it is not already present. + /// + /// If an element equal to `newMember` is already contained in the set, this method has no effect. In this example, + /// a new element is inserted into `classDays`, a set of days of the week. When an existing element is inserted, the + /// `classDays` set does not change. + /// + /// enum DayOfTheWeek: Int { + /// case sunday, monday, tuesday, wednesday, thursday, + /// friday, saturday + /// } + /// + /// var classDays: Set = [.wednesday, .friday] + /// print(classDays.insert(.monday)) + /// // Prints "(true, .monday)" + /// print(classDays) + /// // Prints "[.friday, .wednesday, .monday]" + /// + /// print(classDays.insert(.friday)) + /// // Prints "(false, .friday)" + /// print(classDays) + /// // Prints "[.friday, .wednesday, .monday]" + /// + /// - Parameter newMember: An element to insert into the set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: `(true, newMember)` if `newMember` was not contained in the set. If an element equal to `newMember` + /// was already contained in the set, the method returns `(false, oldMember)`, where `oldMember` is the element that + /// was equal to `newMember`. In some cases, `oldMember` may be distinguishable from `newMember` by identity + /// comparison or some other means. + mutating func insert(_ newMember: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) + -> (inserted: Bool, memberAfterInsert: Self.Element) + /// Removes the given element and any elements subsumed by the given element. + /// + /// - Parameter member: The element of the set to remove. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: For ordinary sets, an element equal to `member` if `member` is contained in the set; otherwise, + /// `nil`. In some cases, a returned element may be distinguishable from `member` by identity comparison or some + /// other means. + /// + /// For sets where the set type and element type are the same, like + /// `OptionSet` types, this method returns any intersection between the set + /// and `[member]`, or `nil` if the intersection is empty. + mutating func remove(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element? + /// Inserts the given element into the set unconditionally. + /// + /// If an element equal to `newMember` is already contained in the set, + /// `newMember` replaces the existing element. In this example, an existing + /// element is inserted into `classDays`, a set of days of the week. + /// + /// enum DayOfTheWeek: Int { + /// case sunday, monday, tuesday, wednesday, thursday, + /// friday, saturday + /// } + /// + /// var classDays: Set = [.monday, .wednesday, .friday] + /// print(classDays.update(with: .monday)) + /// // Prints "Optional(.monday)" + /// + /// - Parameter newMember: An element to insert into the set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: For ordinary sets, an element equal to `newMember` if the set + /// already contained such a member; otherwise, `nil`. In some cases, the + /// returned element may be distinguishable from `newMember` by identity + /// comparison or some other means. + /// + /// For sets where the set type and element type are the same, like + /// `OptionSet` types, this method returns any intersection between the + /// set and `[newMember]`, or `nil` if the intersection is empty. + mutating func update(with newMember: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element? + /// Inserts the given element into the set unconditionally. + /// + /// If an element equal to `customOption` is already contained in the set, `customOption` replaces the existing + /// element. Otherwise - updates the set contents and fills ``customOptionsByRawValue`` accordingly. + /// + /// - Parameter customOption: An element to insert into the set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: For ordinary sets, an element equal to `customOption` if the set already contained such a member; + /// otherwise, `nil`. In some cases, the returned element may be distinguishable from `customOption` by identity + /// comparison or some other means. + /// + /// For sets where the set type and element type are the same, like + /// `OptionSet` types, this method returns any intersection between the + /// set and `[customOption]`, or `nil` if the intersection is empty. + mutating func update(customOption: (RawValue, CustomValue), comparisonPolicy: CustomOptionComparisonPolicy) -> Self + .Element? + /// Returns a Boolean value that indicates whether the set is a subset of + /// another set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let attendees: Set = ["Alicia", "Bethany", "Diana"] + /// print(attendees.isSubset(of: employees)) + /// // Prints "true" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + func isSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool + /// Returns a Boolean value that indicates whether the set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let attendees: Set = ["Alicia", "Bethany", "Diana"] + /// print(employees.isSuperset(of: attendees)) + /// // Prints "true" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: `true` if the set is a superset of `possibleSubset`; + /// otherwise, `false`. + func isSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool + /// Returns a Boolean value that indicates whether this set is a strict + /// subset of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let attendees: Set = ["Alicia", "Bethany", "Diana"] + /// print(attendees.isStrictSubset(of: employees)) + /// // Prints "true" + /// + /// // A set is never a strict subset of itself: + /// print(attendees.isStrictSubset(of: attendees)) + /// // Prints "false" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: `true` if the set is a strict subset of `other`; otherwise, + /// `false`. + func isStrictSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool + /// Returns a Boolean value that indicates whether this set is a strict + /// superset of the given set. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let attendees: Set = ["Alicia", "Bethany", "Diana"] + /// print(employees.isStrictSuperset(of: attendees)) + /// // Prints "true" + /// + /// // A set is never a strict superset of itself: + /// print(employees.isStrictSuperset(of: employees)) + /// // Prints "false" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: `true` if the set is a strict superset of `other`; otherwise, + /// `false`. + func isStrictSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// In the following example, the `employees` set is disjoint with the + /// `visitors` set because no name appears in both sets. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let visitors: Set = ["Marcia", "Nathaniel", "Olivia"] + /// print(employees.isDisjoint(with: visitors)) + /// // Prints "true" + /// + /// - Parameter other: A set of the same type as the current set. + /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. + /// - Returns: `true` if the set has no elements in common with `other`; + /// otherwise, `false`. + func isDisjoint(with other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool +} + +extension CustomValueOptionSet where Self == Self.Element { + // MARK: Implemented methods + + private func customOptionIsEqual( + _ lhs: [RawValue: CustomValue], + _ rhs: [RawValue: CustomValue], + key: RawValue, + policy: CustomOptionComparisonPolicy + ) -> Bool { + switch policy { + case .equal: + return lhs[key] == rhs[key] + case .equalOrNull: + return lhs[key] == rhs[key] || lhs[key] == nil || rhs[key] == nil + case .rawValueEqual: + return true + } + } + + @discardableResult + public func contains(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + let intersection = rawValue & member.rawValue + guard intersection != 0 else { + return false + } + + for offset in 0.. (inserted: Bool, memberAfterInsert: Self.Element) { + if contains(newMember, comparisonPolicy: comparisonPolicy) { + return (false, intersection(newMember, comparisonPolicy: comparisonPolicy)) + } else { + rawValue = rawValue | newMember.rawValue + customOptionsByRawValue.merge(newMember.customOptionsByRawValue) { current, _ in current } + return (true, newMember) + } + } + + @discardableResult @inlinable + public mutating func remove(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self + .Element? { + let intersection = intersection(member, comparisonPolicy: comparisonPolicy) + if intersection.rawValue == 0 { + return nil + } else { + rawValue -= intersection.rawValue + customOptionsByRawValue = customOptionsByRawValue.filter { key, _ in + rawValue & key != 0 + } + return intersection + } + } + + @discardableResult @inlinable + public mutating func update(with newMember: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self + .Element? { + let intersection = intersection(newMember, comparisonPolicy: comparisonPolicy) + + if intersection.rawValue == 0 { + // insert + rawValue = rawValue | newMember.rawValue + customOptionsByRawValue.merge(newMember.customOptionsByRawValue) { current, _ in current } + return nil + } else { + // update + rawValue = rawValue | newMember.rawValue + customOptionsByRawValue.merge(intersection.customOptionsByRawValue) { _, new in new } + return intersection + } + } + + public mutating func formIntersection(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) { + rawValue = rawValue & other.rawValue + customOptionsByRawValue = customOptionsByRawValue.reduce(into: [:]) { partialResult, item in + if customOptionIsEqual( + customOptionsByRawValue, + other.customOptionsByRawValue, + key: item.key, + policy: comparisonPolicy + ) { + partialResult[item.key] = item.value + } else if rawValue & item.key != 0 { + rawValue -= item.key + } + } + } + + public mutating func subtract(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) { + rawValue = rawValue ^ (rawValue & other.rawValue) + customOptionsByRawValue = customOptionsByRawValue.reduce(into: [:]) { partialResult, item in + if !customOptionIsEqual( + customOptionsByRawValue, + other.customOptionsByRawValue, + key: item.key, + policy: comparisonPolicy + ) { + partialResult[item.key] = item.value + } + } + } + + // MARK: Deferring methods + + @discardableResult @inlinable + public func union(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element { + var union = self + union.formUnion(other, comparisonPolicy: comparisonPolicy) + return union + } + + @discardableResult @inlinable + public func intersection(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element { + var intersection = self + intersection.formIntersection(other, comparisonPolicy: comparisonPolicy) + return intersection + } + + @discardableResult @inlinable + public func symmetricDifference(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self + .Element { + var difference = self + difference.formSymmetricDifference(other, comparisonPolicy: comparisonPolicy) + return difference + } + + @discardableResult @inlinable + public mutating func update( + customOption: (RawValue, CustomValue), + comparisonPolicy: CustomOptionComparisonPolicy + ) -> Self.Element? { + var newMember = Self(rawValue: customOption.0) + newMember.customOptionsByRawValue[customOption.0] = customOption.1 + return update(with: newMember, comparisonPolicy: comparisonPolicy) + } + + @inlinable + public mutating func formUnion(_ other: Self, comparisonPolicy: CustomOptionComparisonPolicy) { + _ = update(with: other, comparisonPolicy: comparisonPolicy) + } + + @inlinable + public mutating func formSymmetricDifference( + _ other: Self.Element, + comparisonPolicy: CustomOptionComparisonPolicy + ) { + let intersection = intersection(other, comparisonPolicy: comparisonPolicy) + _ = remove(other, comparisonPolicy: comparisonPolicy) + _ = insert( + other.subtracting( + intersection, + comparisonPolicy: comparisonPolicy + ), + comparisonPolicy: comparisonPolicy + ) + } + + @discardableResult @inlinable + public func subtracting(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element { + var substracted = self + substracted.subtract(other, comparisonPolicy: comparisonPolicy) + return substracted + } + + @discardableResult @inlinable + public func isSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + return intersection(other, comparisonPolicy: comparisonPolicy) == self + } + + @discardableResult @inlinable + public func isDisjoint(with other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + return intersection(other, comparisonPolicy: comparisonPolicy).isEmpty + } + + @discardableResult @inlinable + public func isSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + return other.isSubset(of: self, comparisonPolicy: comparisonPolicy) + } + + @discardableResult @inlinable + public func isStrictSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + return isSuperset(of: other, comparisonPolicy: comparisonPolicy) && rawValue > other.rawValue + } + + @discardableResult @inlinable + public func isStrictSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { + return other.isStrictSuperset(of: self, comparisonPolicy: comparisonPolicy) + } +} + +// MARK: - SetAlgebra implementation + +extension CustomValueOptionSet { + @discardableResult @inlinable + public func contains(_ member: Self.Element) -> Bool { + return contains(member, comparisonPolicy: .equal) + } + + @discardableResult @inlinable + public func union(_ other: Self) -> Self { + return union(other, comparisonPolicy: .equal) + } + + @discardableResult @inlinable + public func intersection(_ other: Self) -> Self { + return intersection(other, comparisonPolicy: .equal) + } + + @discardableResult @inlinable + public func symmetricDifference(_ other: Self) -> Self { + return symmetricDifference(other, comparisonPolicy: .equal) + } + + @discardableResult @inlinable + public mutating func insert(_ newMember: Self.Element) -> (inserted: Bool, memberAfterInsert: Self.Element) { + return insert(newMember, comparisonPolicy: .equal) + } + + @discardableResult @inlinable + public mutating func remove(_ member: Self.Element) -> Self.Element? { + return remove(member, comparisonPolicy: .equal) + } + + @discardableResult @inlinable + public mutating func update(with newMember: Self.Element) -> Self.Element? { + return update(with: newMember, comparisonPolicy: .equal) + } + + @inlinable + public mutating func formUnion(_ other: Self) { + formUnion(other, comparisonPolicy: .equal) + } + + @inlinable + public mutating func formIntersection(_ other: Self) { + formIntersection(other, comparisonPolicy: .equal) + } + + @inlinable + public mutating func formSymmetricDifference(_ other: Self) { + formSymmetricDifference(other, comparisonPolicy: .equal) + } + + @discardableResult @inlinable + public func subtracting(_ other: Self) -> Self { + return subtracting(other, comparisonPolicy: .equal) + } + + @discardableResult @inlinable + public func isSubset(of other: Self) -> Bool { + return isSubset(of: other, comparisonPolicy: .equal) + } + + @discardableResult @inlinable + public func isDisjoint(with other: Self) -> Bool { + return isDisjoint(with: other, comparisonPolicy: .equal) + } + + @discardableResult @inlinable + public func isSuperset(of other: Self) -> Bool { + return isSuperset(of: other, comparisonPolicy: .equal) + } + + @inlinable + public mutating func subtract(_ other: Self) { + subtract(other, comparisonPolicy: .equal) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Directions.swift b/ios/Classes/Navigation/MapboxDirections/Directions.swift new file mode 100644 index 000000000..18b89fd45 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Directions.swift @@ -0,0 +1,711 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +typealias JSONDictionary = [String: Any] + +/// Indicates that an error occurred in MapboxDirections. +public let MBDirectionsErrorDomain = "com.mapbox.directions.ErrorDomain" + +/// A `Directions` object provides you with optimal directions between different locations, or waypoints. The directions +/// object passes your request to the [Mapbox Directions API](https://docs.mapbox.com/api/navigation/#directions) and +/// returns the requested information to a closure (block) that you provide. A directions object can handle multiple +/// simultaneous requests. A ``RouteOptions`` object specifies criteria for the results, such as intermediate waypoints, +/// a mode of transportation, or the level of detail to be returned. +/// +/// Each result produced by the directions object is stored in a ``Route`` object. Depending on the ``RouteOptions`` +/// object you provide, each route may include detailed information suitable for turn-by-turn directions, or it may +/// include only high-level information such as the distance, estimated travel time, and name of each leg of the trip. +/// The waypoints that form the request may be conflated with nearby locations, as appropriate; the resulting waypoints +/// are provided to the closure. +@_documentation(visibility: internal) +open class Directions: @unchecked Sendable { + /// A closure (block) to be called when a directions request is complete. + /// + /// - Parameter result: A `Result` enum that represents the ``RouteResponse`` if the request returned successfully, + /// or the error if it did not. + public typealias RouteCompletionHandler = @Sendable ( + _ result: Result + ) -> Void + + /// A closure (block) to be called when a map matching request is complete. + /// + /// - Parameter result: A `Result` enum that represents the ``MapMatchingResponse`` if the request returned + /// successfully, or the error if it did not. + public typealias MatchCompletionHandler = @Sendable ( + _ result: Result + ) -> Void + + /// A closure (block) to be called when a directions refresh request is complete. + /// + /// - parameter credentials: An object containing the credentials used to make the request. + /// - parameter result: A `Result` enum that represents the ``RouteRefreshResponse`` if the request returned + /// successfully, or the error if it did not. + public typealias RouteRefreshCompletionHandler = @Sendable ( + _ credentials: Credentials, + _ result: Result + ) -> Void + + // MARK: Creating a Directions Object + + /// The shared directions object. + /// + /// To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be + /// specified in the `MBXAccessToken` key in the main application bundle’s Info.plist. + public static let shared: Directions = .init() + + /// The Authorization & Authentication credentials that are used for this service. + /// + /// If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. + public let credentials: Credentials + + private let urlSession: URLSession + private let processingQueue: DispatchQueue + + /// Creates a new instance of Directions object. + /// - Parameters: + /// - credentials: Credentials that will be used to make API requests to Mapbox Directions API. + /// - urlSession: URLSession that will be used to submit API requests to Mapbox Directions API. + /// - processingQueue: A DispatchQueue that will be used for CPU intensive work. + public init( + credentials: Credentials = .init(), + urlSession: URLSession = .shared, + processingQueue: DispatchQueue = .global(qos: .userInitiated) + ) { + self.credentials = credentials + self.urlSession = urlSession + self.processingQueue = processingQueue + } + + // MARK: Getting Directions + + /// Begins asynchronously calculating routes using the given options and delivers the results to a closure. + /// + /// This method retrieves the routes asynchronously from the [Mapbox Directions + /// API](https://www.mapbox.com/api-documentation/navigation/#directions) over a network connection. If a connection + /// error or server error occurs, details about the error are passed into the given completion handler in lieu of + /// the routes. + /// + /// Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). + /// - Parameters: + /// - options: A ``RouteOptions`` object specifying the requirements for the resulting routes. + /// - completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the + /// application’s main thread. + /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to + /// execute, you no longer want the resulting routes, cancel this task. + @discardableResult + open func calculate( + _ options: RouteOptions, + completionHandler: @escaping RouteCompletionHandler + ) -> URLSessionDataTask { + options.fetchStartDate = Date() + let request = urlRequest(forCalculating: options) + let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in + + if let urlError = possibleError as? URLError { + completionHandler(.failure(.network(urlError))) + return + } + + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { + completionHandler(.failure(.invalidResponse(possibleResponse))) + return + } + + guard let data = possibleData else { + completionHandler(.failure(.noData)) + return + } + + self.processingQueue.async { + do { + let decoder = JSONDecoder() + decoder.userInfo = [ + .options: options, + .credentials: self.credentials, + ] + + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = DirectionsError( + code: nil, + message: nil, + response: possibleResponse, + underlyingError: possibleError + ) + completionHandler(.failure(apiError)) + return + } + + guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { + let apiError = DirectionsError( + code: disposition.code, + message: disposition.message, + response: response, + underlyingError: possibleError + ) + completionHandler(.failure(apiError)) + return + } + + let result = try decoder.decode(RouteResponse.self, from: data) + guard result.routes != nil else { + completionHandler(.failure(.unableToRoute)) + return + } + + completionHandler(.success(result)) + } catch { + let bailError = DirectionsError(code: nil, message: nil, response: response, underlyingError: error) + completionHandler(.failure(bailError)) + } + } + } + requestTask.priority = 1 + requestTask.resume() + + return requestTask + } + + /// Begins asynchronously calculating matches using the given options and delivers the results to a closure.This + /// method retrieves the matches asynchronously from the [Mapbox Map Matching + /// API](https://docs.mapbox.com/api/navigation/#map-matching) over a network connection. If a connection error or + /// server error occurs, details about the error are passed into the given completion handler in lieu of the routes. + /// + /// To get ``Route``s based on these matches, use the `calculateRoutes(matching:completionHandler:)` method + /// instead. + /// - Parameters: + /// - options: A ``MatchOptions`` object specifying the requirements for the resulting matches. + /// - completionHandler: The closure (block) to call with the resulting matches. This closure is executed on the + /// application’s main thread. + /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to + /// execute, you no longer want the resulting matches, cancel this task. + @discardableResult + open func calculate( + _ options: MatchOptions, + completionHandler: @escaping MatchCompletionHandler + ) -> URLSessionDataTask { + options.fetchStartDate = Date() + let request = urlRequest(forCalculating: options) + let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in + if let urlError = possibleError as? URLError { + completionHandler(.failure(.network(urlError))) + return + } + + guard let response = possibleResponse, response.mimeType == "application/json" else { + completionHandler(.failure(.invalidResponse(possibleResponse))) + return + } + + guard let data = possibleData else { + completionHandler(.failure(.noData)) + return + } + + self.processingQueue.async { + do { + let decoder = JSONDecoder() + decoder.userInfo = [ + .options: options, + .credentials: self.credentials, + ] + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = DirectionsError( + code: nil, + message: nil, + response: possibleResponse, + underlyingError: possibleError + ) + completionHandler(.failure(apiError)) + return + } + + guard disposition.code == "Ok" else { + let apiError = DirectionsError( + code: disposition.code, + message: disposition.message, + response: response, + underlyingError: possibleError + ) + completionHandler(.failure(apiError)) + return + } + + let response = try decoder.decode(MapMatchingResponse.self, from: data) + + guard response.matches != nil else { + completionHandler(.failure(.unableToRoute)) + return + } + + completionHandler(.success(response)) + } catch { + let caughtError = DirectionsError.unknown( + response: response, + underlying: error, + code: nil, + message: nil + ) + completionHandler(.failure(caughtError)) + } + } + } + requestTask.priority = 1 + requestTask.resume() + + return requestTask + } + + /// Begins asynchronously calculating routes that match the given options and delivers the results to a closure. + /// + /// This method retrieves the routes asynchronously from the [Mapbox Map Matching + /// API](https://docs.mapbox.com/api/navigation/#map-matching) over a network connection. If a connection error or + /// server error occurs, details about the error are passed into the given completion handler in lieu of the routes. + /// + /// To get the ``Match``es that these routes are based on, use the `calculate(_:completionHandler:)` method instead. + /// - Parameters: + /// - options: A ``MatchOptions`` object specifying the requirements for the resulting match. + /// - completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the + /// application’s main thread. + /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to + /// execute, you no longer want the resulting routes, cancel this task. + @discardableResult + open func calculateRoutes( + matching options: MatchOptions, + completionHandler: @escaping RouteCompletionHandler + ) -> URLSessionDataTask { + options.fetchStartDate = Date() + let request = urlRequest(forCalculating: options) + let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in + if let urlError = possibleError as? URLError { + completionHandler(.failure(.network(urlError))) + return + } + + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { + completionHandler(.failure(.invalidResponse(possibleResponse))) + return + } + + guard let data = possibleData else { + completionHandler(.failure(.noData)) + return + } + + self.processingQueue.async { + do { + let decoder = JSONDecoder() + decoder.userInfo = [ + .options: options, + .credentials: self.credentials, + ] + + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = DirectionsError( + code: nil, + message: nil, + response: possibleResponse, + underlyingError: possibleError + ) + completionHandler(.failure(apiError)) + return + } + + guard disposition.code == "Ok" else { + let apiError = DirectionsError( + code: disposition.code, + message: disposition.message, + response: response, + underlyingError: possibleError + ) + completionHandler(.failure(apiError)) + return + } + + let result = try decoder.decode(MapMatchingResponse.self, from: data) + + let routeResponse = try RouteResponse( + matching: result, + options: options, + credentials: self.credentials + ) + guard routeResponse.routes != nil else { + completionHandler(.failure(.unableToRoute)) + return + } + + completionHandler(.success(routeResponse)) + } catch { + let bailError = DirectionsError(code: nil, message: nil, response: response, underlyingError: error) + completionHandler(.failure(bailError)) + } + } + } + requestTask.priority = 1 + requestTask.resume() + + return requestTask + } + + /// Begins asynchronously refreshing the route with the given identifier, optionally starting from an arbitrary leg + /// along the route. + /// + /// This method retrieves skeleton route data asynchronously from the Mapbox Directions Refresh API over a network + /// connection. If a connection error or server error occurs, details about the error are passed into the given + /// completion handler in lieu of the routes. + /// + /// - Precondition: Set ``RouteOptions/refreshingEnabled`` to `true` when calculating the original route. + /// - Parameters: + /// - responseIdentifier: The ``RouteResponse/identifier`` value of the ``RouteResponse`` that contains the route + /// to refresh. + /// - routeIndex: The index of the route to refresh in the original ``RouteResponse/routes`` array. + /// - startLegIndex: The index of the leg in the route at which to begin refreshing. The response will omit any + /// leg before this index and refresh any leg from this index to the end of the route. If this argument is omitted, + /// the entire route is refreshed. + /// - completionHandler: The closure (block) to call with the resulting skeleton route data. This closure is + /// executed on the application’s main thread. + /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to + /// execute, you no longer want the resulting skeleton routes, cancel this task. + @discardableResult + open func refreshRoute( + responseIdentifier: String, + routeIndex: Int, + fromLegAtIndex startLegIndex: Int = 0, + completionHandler: @escaping RouteRefreshCompletionHandler + ) -> URLSessionDataTask? { + _refreshRoute( + responseIdentifier: responseIdentifier, + routeIndex: routeIndex, + fromLegAtIndex: startLegIndex, + currentRouteShapeIndex: nil, + completionHandler: completionHandler + ) + } + + /// Begins asynchronously refreshing the route with the given identifier, optionally starting from an arbitrary leg + /// and point along the route. + /// + /// This method retrieves skeleton route data asynchronously from the Mapbox Directions Refresh API over a network + /// connection. If a connection error or server error occurs, details about the error are passed into the given + /// completion handler in lieu of the routes. + /// + /// - Precondition: Set ``RouteOptions/refreshingEnabled`` to `true` when calculating the original route. + /// - Parameters: + /// - responseIdentifier: The ``RouteResponse/identifier`` value of the ``RouteResponse`` that contains the route + /// to refresh. + /// - routeIndex: The index of the route to refresh in the original ``RouteResponse/routes`` array. + /// - startLegIndex: The index of the leg in the route at which to begin refreshing. The response will omit any + /// leg before this index and refresh any leg from this index to the end of the route. If this argument is omitted, + /// the entire route is refreshed. + /// - currentRouteShapeIndex: The index of the route geometry at which to begin refreshing. Indexed geometry must + /// be contained by the leg at `startLegIndex`. + /// - completionHandler: The closure (block) to call with the resulting skeleton route data. This closure is + /// executed on the application’s main thread. + /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to + /// execute, you no longer want the resulting skeleton routes, cancel this task. + @discardableResult + open func refreshRoute( + responseIdentifier: String, + routeIndex: Int, + fromLegAtIndex startLegIndex: Int = 0, + currentRouteShapeIndex: Int, + completionHandler: @escaping RouteRefreshCompletionHandler + ) -> URLSessionDataTask? { + _refreshRoute( + responseIdentifier: responseIdentifier, + routeIndex: routeIndex, + fromLegAtIndex: startLegIndex, + currentRouteShapeIndex: currentRouteShapeIndex, + completionHandler: completionHandler + ) + } + + private func _refreshRoute( + responseIdentifier: String, + routeIndex: Int, + fromLegAtIndex startLegIndex: Int, + currentRouteShapeIndex: Int?, + completionHandler: @escaping RouteRefreshCompletionHandler + ) -> URLSessionDataTask? { + let request: URLRequest = if let currentRouteShapeIndex { + urlRequest( + forRefreshing: responseIdentifier, + routeIndex: routeIndex, + fromLegAtIndex: startLegIndex, + currentRouteShapeIndex: currentRouteShapeIndex + ) + } else { + urlRequest( + forRefreshing: responseIdentifier, + routeIndex: routeIndex, + fromLegAtIndex: startLegIndex + ) + } + let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in + if let urlError = possibleError as? URLError { + DispatchQueue.main.async { + completionHandler(self.credentials, .failure(.network(urlError))) + } + return + } + + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { + DispatchQueue.main.async { + completionHandler(self.credentials, .failure(.invalidResponse(possibleResponse))) + } + return + } + + guard let data = possibleData else { + DispatchQueue.main.async { + completionHandler(self.credentials, .failure(.noData)) + } + return + } + + self.processingQueue.async { + do { + let decoder = JSONDecoder() + decoder.userInfo = [ + .responseIdentifier: responseIdentifier, + .routeIndex: routeIndex, + .startLegIndex: startLegIndex, + .credentials: self.credentials, + ] + + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = DirectionsError( + code: nil, + message: nil, + response: possibleResponse, + underlyingError: possibleError + ) + + DispatchQueue.main.async { + completionHandler(self.credentials, .failure(apiError)) + } + return + } + + guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { + let apiError = DirectionsError( + code: disposition.code, + message: disposition.message, + response: response, + underlyingError: possibleError + ) + DispatchQueue.main.async { + completionHandler(self.credentials, .failure(apiError)) + } + return + } + + let result = try decoder.decode(RouteRefreshResponse.self, from: data) + + DispatchQueue.main.async { + completionHandler(self.credentials, .success(result)) + } + } catch { + DispatchQueue.main.async { + let bailError = DirectionsError( + code: nil, + message: nil, + response: response, + underlyingError: error + ) + completionHandler(self.credentials, .failure(bailError)) + } + } + } + } + requestTask.priority = 1 + requestTask.resume() + return requestTask + } + + open func urlRequest( + forRefreshing responseIdentifier: String, + routeIndex: Int, + fromLegAtIndex startLegIndex: Int + ) -> URLRequest { + _urlRequest( + forRefreshing: responseIdentifier, + routeIndex: routeIndex, + fromLegAtIndex: startLegIndex, + currentRouteShapeIndex: nil + ) + } + + open func urlRequest( + forRefreshing responseIdentifier: String, + routeIndex: Int, + fromLegAtIndex startLegIndex: Int, + currentRouteShapeIndex: Int + ) -> URLRequest { + _urlRequest( + forRefreshing: responseIdentifier, + routeIndex: routeIndex, + fromLegAtIndex: startLegIndex, + currentRouteShapeIndex: currentRouteShapeIndex + ) + } + + private func _urlRequest( + forRefreshing responseIdentifier: String, + routeIndex: Int, + fromLegAtIndex startLegIndex: Int, + currentRouteShapeIndex: Int? + ) -> URLRequest { + var params: [URLQueryItem] = credentials.authenticationParams + if let currentRouteShapeIndex { + params.append(URLQueryItem(name: "current_route_geometry_index", value: String(currentRouteShapeIndex))) + } + + var unparameterizedURL = URL( + string: "directions-refresh/v1/\(ProfileIdentifier.automobileAvoidingTraffic.rawValue)", + relativeTo: credentials.host + )! + unparameterizedURL.appendPathComponent(responseIdentifier) + unparameterizedURL.appendPathComponent(String(routeIndex)) + unparameterizedURL.appendPathComponent(String(startLegIndex)) + var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! + + components.queryItems = params + + let getURL = components.url! + var request = URLRequest(url: getURL) + request.setupUserAgentString() + return request + } + + /// The GET HTTP URL used to fetch the routes from the API. + /// + /// After requesting the URL returned by this method, you can parse the JSON data in the response and pass it into + /// the ``Route/init(from:)`` initializer. Alternatively, you can use the ``calculate(_:completionHandler:)-8je4q`` + /// method, which automatically sends the request and parses the response. + /// - Parameter options: A ``DirectionsOptions`` object specifying the requirements for the resulting routes. + /// - Returns: The URL to send the request to. + open func url(forCalculating options: DirectionsOptions) -> URL { + return url(forCalculating: options, httpMethod: "GET") + } + + /// The HTTP URL used to fetch the routes from the API using the specified HTTP method. + /// + /// The query part of the URL is generally suitable for GET requests. However, if the URL is exceptionally long, it + /// may be more appropriate to send a POST request to a URL without the query part, relegating the query to the body + /// of the HTTP request. Use the `urlRequest(forCalculating:)` method to get an HTTP request that is a GET or POST + /// request as necessary. + /// + /// After requesting the URL returned by this method, you can parse the JSON data in the response and pass it into + /// the ``Route/init(from:)`` initializer. Alternatively, you can use the ``calculate(_:completionHandler:)-8je4q`` + /// method, which automatically sends the request and parses the response. + /// - Parameters: + /// - options: A ``DirectionsOptions`` object specifying the requirements for the resulting routes. + /// - httpMethod: The HTTP method to use. The value of this argument should match the `URLRequest.httpMethod` of + /// the request you send. Currently, only GET and POST requests are supported by the API. + /// - Returns: The URL to send the request to. + open func url(forCalculating options: DirectionsOptions, httpMethod: String) -> URL { + Self.url(forCalculating: options, credentials: credentials, httpMethod: httpMethod) + } + + /// The GET HTTP URL used to fetch the routes from the API. + /// + /// After requesting the URL returned by this method, you can parse the JSON data in the response and pass it into + /// the ``Route/init(from:)`` initializer. Alternatively, you can use the + /// ``calculate(_:completionHandler:)-8je4q`` method, which automatically sends the request and parses the response. + /// + /// - parameter options: A ``DirectionsOptions`` object specifying the requirements for the resulting routes. + /// - parameter credentials: ``Credentials`` data applied to the request. + /// - returns: The URL to send the request to. + public static func url(forCalculating options: DirectionsOptions, credentials: Credentials) -> URL { + return url(forCalculating: options, credentials: credentials, httpMethod: "GET") + } + + /// The HTTP URL used to fetch the routes from the API using the specified HTTP method. + /// + /// The query part of the URL is generally suitable for GET requests. However, if the URL is exceptionally long, it + /// may be more appropriate to send a POST request to a URL without the query part, relegating the query to the body + /// of the HTTP request. Use the `urlRequest(forCalculating:)` method to get an HTTP request that is a GET or POST + /// request as necessary. + /// + /// After requesting the URL returned by this method, you can parse the JSON data in the response and pass it into + /// the ``Route/init(from:)`` initializer. Alternatively, you can use the ``calculate(_:completionHandler:)-8je4q`` + /// method, which automatically sends the request and parses the response. + /// - Parameters: + /// - options: A ``DirectionsOptions`` object specifying the requirements for the resulting routes. + /// - credentials: ``Credentials`` data applied to the request. + /// - httpMethod: The HTTP method to use. The value of this argument should match the `URLRequest.httpMethod` of + /// the request you send. Currently, only GET and POST requests are supported by the API. + /// - Returns: The URL to send the request to. + public static func url( + forCalculating options: DirectionsOptions, + credentials: Credentials, + httpMethod: String + ) -> URL { + let includesQuery = httpMethod != "POST" + var params = (includesQuery ? options.urlQueryItems : []) + params.append(contentsOf: credentials.authenticationParams) + + let unparameterizedURL = URL(path: includesQuery ? options.path : options.abridgedPath, host: credentials.host) + var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! + components.queryItems = params + return components.url! + } + + /// The HTTP request used to fetch the routes from the API. + /// + /// The returned request is a GET or POST request as necessary to accommodate URL length limits. + /// + /// After sending the request returned by this method, you can parse the JSON data in the response and pass it into + /// the ``Route.init(json:waypoints:profileIdentifier:)`` initializer. Alternatively, you can use the + /// `calculate(_:options:)` method, which automatically sends the request and parses the response. + /// + /// - Parameter options: A ``DirectionsOptions`` object specifying the requirements for the resulting routes. + /// - Returns: A GET or POST HTTP request to calculate the specified options. + open func urlRequest(forCalculating options: DirectionsOptions) -> URLRequest { + if options.waypoints.count < 2 { assertionFailure("waypoints array requires at least 2 waypoints") } + let getURL = Self.url(forCalculating: options, credentials: credentials, httpMethod: "GET") + var request = URLRequest(url: getURL) + if getURL.absoluteString.count > MaximumURLLength { + request.url = Self.url(forCalculating: options, credentials: credentials, httpMethod: "POST") + + let body = options.httpBody.data(using: .utf8) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + request.httpBody = body + } + request.setupUserAgentString() + return request + } +} + +@available(*, unavailable) +extension Directions: @unchecked Sendable {} + +/// Keys to pass to populate a `userInfo` dictionary, which is passed to the `JSONDecoder` upon trying to decode a +/// ``RouteResponse``, ``MapMatchingResponse`` or ``RouteRefreshResponse``. +extension CodingUserInfoKey { + public static let options = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.routeOptions")! + public static let httpResponse = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.httpResponse")! + public static let credentials = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.credentials")! + public static let tracepoints = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.tracepoints")! + + public static let responseIdentifier = + CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.responseIdentifier")! + public static let routeIndex = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.routeIndex")! + public static let startLegIndex = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.startLegIndex")! +} + +extension Credentials { + fileprivate var authenticationParams: [URLQueryItem] { + var params: [URLQueryItem] = [ + URLQueryItem(name: "access_token", value: accessToken), + ] + + if let skuToken { + params.append(URLQueryItem(name: "sku", value: skuToken)) + } + return params + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/DirectionsError.swift b/ios/Classes/Navigation/MapboxDirections/DirectionsError.swift new file mode 100644 index 000000000..fd27a300d --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/DirectionsError.swift @@ -0,0 +1,215 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +/// An error that occurs when calculating directions. +public enum DirectionsError: LocalizedError { + public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { + if let response = response as? HTTPURLResponse { + switch (response.statusCode, code ?? "") { + case (200, "NoRoute"): + self = .unableToRoute + case (200, "NoSegment"): + self = .unableToLocate + case (200, "NoMatch"): + self = .noMatches + case (422, "TooManyCoordinates"): + self = .tooManyCoordinates + case (404, "ProfileNotFound"): + self = .profileNotFound + case (413, _): + self = .requestTooLarge + case (422, "InvalidInput"): + self = .invalidInput(message: message) + case (429, _): + self = .rateLimited( + rateLimitInterval: response.rateLimitInterval, + rateLimit: response.rateLimit, + resetTime: response.rateLimitResetTime + ) + default: + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } else { + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } + + /// There is no network connection available to perform the network request. + case network(_: URLError) + + /// The server returned an empty response. + case noData + + /// The API recieved input that it didn't understand. + case invalidInput(message: String?) + + /// The server returned a response that isn’t correctly formatted. + case invalidResponse(_: URLResponse?) + + /// No route could be found between the specified locations. + /// + /// Make sure it is possible to travel between the locations with the mode of transportation implied by the + /// profileIdentifier option. For example, it is impossible to travel by car from one continent to another without + /// either a land bridge or a ferry connection. + case unableToRoute + + /// The specified coordinates could not be matched to the road network. + /// + /// Try again making sure that your tracepoints lie in close proximity to a road or path. + case noMatches + + /// The request specifies too many coordinates. + /// + /// Try again with fewer coordinates. + case tooManyCoordinates + + /// A specified location could not be associated with a roadway or pathway. + /// + /// Make sure the locations are close enough to a roadway or pathway. Try setting the + /// ``Waypoint/coordinateAccuracy`` property of all the waypoints to `nil`. + case unableToLocate + + /// Unrecognized profile identifier. + /// + /// Make sure the ``DirectionsOptions/profileIdentifier`` option is set to one of the predefined values, such as + /// ``ProfileIdentifier/automobile``. + case profileNotFound + + /// The request is too large. + /// + /// Try specifying fewer waypoints or giving the waypoints shorter names. + case requestTooLarge + + /// Too many requests have been made with the same access token within a certain period of time. + /// + /// Wait before retrying. + case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) + + /// Unknown error case. Look at associated values for more details. + case unknown(response: URLResponse?, underlying: Error?, code: String?, message: String?) + + public var failureReason: String? { + switch self { + case .network: + return "The client does not have a network connection to the server." + case .noData: + return "The server returned an empty response." + case .invalidInput(let message): + return message + case .invalidResponse: + return "The server returned a response that isn’t correctly formatted." + case .unableToRoute: + return "No route could be found between the specified locations." + case .noMatches: + return "The specified coordinates could not be matched to the road network." + case .tooManyCoordinates: + return "The request specifies too many coordinates." + case .unableToLocate: + return "A specified location could not be associated with a roadway or pathway." + case .profileNotFound: + return "Unrecognized profile identifier." + case .requestTooLarge: + return "The request is too large." + case .rateLimited(rateLimitInterval: let interval, rateLimit: let limit, _): + guard let interval, let limit else { + return "Too many requests." + } +#if os(Linux) + let formattedInterval = "\(interval) seconds" +#else + let intervalFormatter = DateComponentsFormatter() + intervalFormatter.unitsStyle = .full + let formattedInterval = intervalFormatter.string(from: interval) ?? "\(interval) seconds" +#endif + let formattedCount = NumberFormatter.localizedString(from: NSNumber(value: limit), number: .decimal) + return "More than \(formattedCount) requests have been made with this access token within a period of \(formattedInterval)." + case .unknown(_, underlying: let error, _, let message): + return message + ?? (error as NSError?)?.userInfo[NSLocalizedFailureReasonErrorKey] as? String + ?? HTTPURLResponse.localizedString(forStatusCode: (error as NSError?)?.code ?? -1) + } + } + + public var recoverySuggestion: String? { + switch self { + case .network(_), .noData, .invalidInput, .invalidResponse: + return nil + case .unableToRoute: + return "Make sure it is possible to travel between the locations with the mode of transportation implied by the profileIdentifier option. For example, it is impossible to travel by car from one continent to another without either a land bridge or a ferry connection." + case .noMatches: + return "Try again making sure that your tracepoints lie in close proximity to a road or path." + case .tooManyCoordinates: + return "Try again with 100 coordinates or fewer." + case .unableToLocate: + return "Make sure the locations are close enough to a roadway or pathway. Try setting the coordinateAccuracy property of all the waypoints to nil." + case .profileNotFound: + return "Make sure the profileIdentifier option is set to one of the provided constants, such as ProfileIdentifier.automobile." + case .requestTooLarge: + return "Try specifying fewer waypoints or giving the waypoints shorter names." + case .rateLimited(rateLimitInterval: _, rateLimit: _, resetTime: let rolloverTime): + guard let rolloverTime else { + return nil + } + let formattedDate: String = DateFormatter.localizedString( + from: rolloverTime, + dateStyle: .long, + timeStyle: .long + ) + return "Wait until \(formattedDate) before retrying." + case .unknown(_, underlying: let error, _, _): + return (error as NSError?)?.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String + } + } +} + +extension DirectionsError: Equatable { + public static func == (lhs: DirectionsError, rhs: DirectionsError) -> Bool { + switch (lhs, rhs) { + case (.noData, .noData), + (.unableToRoute, .unableToRoute), + (.noMatches, .noMatches), + (.tooManyCoordinates, .tooManyCoordinates), + (.unableToLocate, .unableToLocate), + (.profileNotFound, .profileNotFound), + (.requestTooLarge, .requestTooLarge): + return true + case (.network(let lhsError), .network(let rhsError)): + return lhsError == rhsError + case (.invalidResponse(let lhsResponse), .invalidResponse(let rhsResponse)): + return lhsResponse == rhsResponse + case (.invalidInput(let lhsMessage), .invalidInput(let rhsMessage)): + return lhsMessage == rhsMessage + case ( + .rateLimited(let lhsRateLimitInterval, let lhsRateLimit, let lhsResetTime), + .rateLimited(let rhsRateLimitInterval, let rhsRateLimit, let rhsResetTime) + ): + return lhsRateLimitInterval == rhsRateLimitInterval + && lhsRateLimit == rhsRateLimit + && lhsResetTime == rhsResetTime + case ( + .unknown(let lhsResponse, let lhsUnderlying, let lhsCode, let lhsMessage), + .unknown(let rhsResponse, let rhsUnderlying, let rhsCode, let rhsMessage) + ): + return lhsResponse == rhsResponse + && type(of: lhsUnderlying) == type(of: rhsUnderlying) + && lhsUnderlying?.localizedDescription == rhsUnderlying?.localizedDescription + && lhsCode == rhsCode + && lhsMessage == rhsMessage + default: + return false + } + } +} + +/// An error that occurs when encoding or decoding a type defined by the MapboxDirections framework. +public enum DirectionsCodingError: Error { + /// Decoding this type requires the `Decoder.userInfo` dictionary to contain the ``Swift/CodingUserInfoKey/options`` + /// key. + case missingOptions + + /// Decoding this type requires the `Decoder.userInfo` dictionary to contain the + /// ``Swift/CodingUserInfoKey/credentials`` key. + case missingCredentials +} diff --git a/ios/Classes/Navigation/MapboxDirections/DirectionsOptions.swift b/ios/Classes/Navigation/MapboxDirections/DirectionsOptions.swift new file mode 100644 index 000000000..b95f23505 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/DirectionsOptions.swift @@ -0,0 +1,610 @@ +import Foundation +import Turf + +/// Maximum length of an HTTP request URL for the purposes of switching from GET to POST. +/// +/// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-general +let MaximumURLLength = 1024 * 8 + +/// A ``RouteShapeFormat`` indicates the format of a route or match shape in the raw HTTP response. +public enum RouteShapeFormat: String, Codable, Equatable, Sendable { + /// The route’s shape is delivered in [GeoJSON](http://geojson.org/) format. + /// + /// This standard format is human-readable and can be parsed straightforwardly, but it is far more verbose than + /// ``RouteShapeFormat/polyline``. + case geoJSON = "geojson" + /// The route’s shape is delivered in [encoded polyline + /// algorithm](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) format with + /// 1×10−5 precision. + /// + /// This machine-readable format is considerably more compact than ``RouteShapeFormat/geoJSON`` but less precise + /// than ``RouteShapeFormat/polyline6``. + case polyline + /// The route’s shape is delivered in [encoded polyline + /// algorithm](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) format with + /// 1×10−6 precision. + /// + /// This format is an order of magnitude more precise than ``RouteShapeFormat/polyline``. + case polyline6 + + static let `default` = RouteShapeFormat.polyline +} + +/// A ``RouteShapeResolution`` indicates the level of detail in a route’s shape, or whether the shape is present at all. +public enum RouteShapeResolution: String, Codable, Equatable, Sendable { + /// The route’s shape is omitted. + /// + /// Specify this resolution if you do not intend to show the route line to the user or analyze the route line in any + /// way. + case none = "false" + /// The route’s shape is simplified. + /// + /// This resolution considerably reduces the size of the response. The resulting shape is suitable for display at a + /// low zoom level, but it lacks the detail necessary for focusing on individual segments of the route. + case low = "simplified" + /// The route’s shape is as detailed as possible. + /// + /// The resulting shape is equivalent to concatenating the shapes of all the route’s consitituent steps. You can + /// focus on individual segments of this route while faithfully representing the path of the route. If you only + /// intend to show a route overview and do not need to analyze the route line in any way, consider specifying + /// ``RouteShapeResolution/low`` instead to considerably reduce the size of the response. + case full +} + +/// A system of units of measuring distances and other quantities. +public enum MeasurementSystem: String, Codable, Equatable, Sendable { + /// U.S. customary and British imperial units. + /// + /// Distances are measured in miles and feet. + case imperial + + /// The metric system. + /// + /// Distances are measured in kilometers and meters. + case metric +} + +@available(*, deprecated, renamed: "DirectionsPriority") +public typealias MBDirectionsPriority = DirectionsPriority + +/// A number that influences whether a route should prefer or avoid roadways or pathways of a given type. +public struct DirectionsPriority: Hashable, RawRepresentable, Codable, Equatable, Sendable { + public init(rawValue: Double) { + self.rawValue = rawValue + } + + public var rawValue: Double + + /// The priority level with which a route avoids a particular type of roadway or pathway. + public static let low = DirectionsPriority(rawValue: -1.0) + + /// The priority level with which a route neither avoids nor prefers a particular type of roadway or pathway. + public static let medium = DirectionsPriority(rawValue: 0.0) + + /// The priority level with which a route prefers a particular type of roadway or pathway. + public static let high = DirectionsPriority(rawValue: 1.0) +} + +/// Options for calculating results from the Mapbox Directions service. +/// +/// You do not create instances of this class directly. Instead, create instances of ``MatchOptions`` or +/// ``RouteOptions``. +open class DirectionsOptions: Codable, @unchecked Sendable { + // MARK: Creating a Directions Options Object + + /// Initializes an options object for routes between the given waypoints and an optional profile identifier. + /// + /// Do not call ``DirectionsOptions/init(waypoints:profileIdentifier:queryItems:)`` directly; instead call the + /// corresponding + /// initializer of ``RouteOptions`` or ``MatchOptions``. + /// - Parameters: + /// - waypoints: An array of ``Waypoint`` objects representing locations that the route should visit in + /// chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 + /// waypoints. (Some profiles, such as ``ProfileIdentifier/automobileAvoidingTraffic``, [may have lower + /// limits](https://docs.mapbox.com/api/navigation/#directions).) + /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. + /// ``ProfileIdentifier/automobile`` is used by default. + /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. + public required init( + waypoints: [Waypoint], + profileIdentifier: ProfileIdentifier? = nil, + queryItems: [URLQueryItem]? = nil + ) { + var waypoints = waypoints + self.profileIdentifier = profileIdentifier ?? .automobile + + guard let queryItems else { + self.waypoints = waypoints + return + } + + let mappedQueryItems = [String: String]( + queryItems.compactMap { + guard let value = $0.value else { return nil } + return ($0.name, value) + }, + uniquingKeysWith: { _, latestValue in + return latestValue + } + ) + + if let mappedValue = mappedQueryItems[CodingKeys.shapeFormat.stringValue], + let shapeFormat = RouteShapeFormat(rawValue: mappedValue) + { + self.shapeFormat = shapeFormat + } + if let mappedValue = mappedQueryItems[CodingKeys.routeShapeResolution.stringValue], + let routeShapeResolution = RouteShapeResolution(rawValue: mappedValue) + { + self.routeShapeResolution = routeShapeResolution + } + if mappedQueryItems[CodingKeys.includesSteps.stringValue] == "true" { + self.includesSteps = true + } + if let mappedValue = mappedQueryItems[CodingKeys.locale.stringValue] { + self.locale = Locale(identifier: mappedValue) + } + if mappedQueryItems[CodingKeys.includesSpokenInstructions.stringValue] == "true" { + self.includesSpokenInstructions = true + } + if let mappedValue = mappedQueryItems[CodingKeys.distanceMeasurementSystem.stringValue], + let measurementSystem = MeasurementSystem(rawValue: mappedValue) + { + self.distanceMeasurementSystem = measurementSystem + } + if mappedQueryItems[CodingKeys.includesVisualInstructions.stringValue] == "true" { + self.includesVisualInstructions = true + } + if let mappedValue = mappedQueryItems[CodingKeys.attributeOptions.stringValue], + let attributeOptions = AttributeOptions(descriptions: mappedValue.components(separatedBy: ",")) + { + self.attributeOptions = attributeOptions + } + if let mappedValue = mappedQueryItems["waypoints"] { + let indicies = mappedValue.components(separatedBy: ";").compactMap { Int($0) } + if !indicies.isEmpty { + for index in waypoints.indices { + waypoints[index].separatesLegs = indicies.contains(index) + } + } + } + + let waypointsData = [ + mappedQueryItems["approaches"]?.components(separatedBy: ";"), + mappedQueryItems["bearings"]?.components(separatedBy: ";"), + mappedQueryItems["radiuses"]?.components(separatedBy: ";"), + mappedQueryItems["waypoint_names"]?.components(separatedBy: ";"), + mappedQueryItems["snapping_include_closures"]?.components(separatedBy: ";"), + mappedQueryItems["snapping_include_static_closures"]?.components(separatedBy: ";"), + ] as [[String]?] + + let getElement: ((_ array: [String]?, _ index: Int) -> String?) = { array, index in + if array?.count ?? -1 > index { + return array?[index] + } + return nil + } + + for waypointIndex in waypoints.indices { + if let approach = getElement(waypointsData[0], waypointIndex) { + waypoints[waypointIndex].allowsArrivingOnOppositeSide = approach == "unrestricted" ? true : false + } + + if let descriptions = getElement(waypointsData[1], waypointIndex)?.components(separatedBy: ",") { + waypoints[waypointIndex].heading = LocationDirection(descriptions.first!) + waypoints[waypointIndex].headingAccuracy = LocationDirection(descriptions.last!) + } + + if let accuracy = getElement(waypointsData[2], waypointIndex) { + waypoints[waypointIndex].coordinateAccuracy = LocationAccuracy(accuracy) + } + + if let snaps = getElement(waypointsData[4], waypointIndex) { + waypoints[waypointIndex].allowsSnappingToClosedRoad = snaps == "true" + } + + if let snapsToStaticallyClosed = getElement(waypointsData[5], waypointIndex) { + waypoints[waypointIndex].allowsSnappingToStaticallyClosedRoad = snapsToStaticallyClosed == "true" + } + } + + var separatesLegIndex = 0 + for waypointIndex in waypoints.indices { + guard waypoints[waypointIndex].separatesLegs else { continue } + + if let name = getElement(waypointsData[3], separatesLegIndex) { + waypoints[waypointIndex].name = name + } + separatesLegIndex += 1 + } + + self.waypoints = waypoints + } + + /// Creates new options object by deserializing given `url` + /// + /// Initialization fails if it is unable to extract ``waypoints`` list and ``profileIdentifier``. If other + /// properties are failed to decode - it will just skip them. + /// - Parameter url: An URL, used to make a route request. + public convenience init?(url: URL) { + guard url.pathComponents.count >= 3 else { + return nil + } + + let waypointsString = url.lastPathComponent.replacingOccurrences(of: ".json", with: "") + let waypoints: [Waypoint] = waypointsString.components(separatedBy: ";").compactMap { + let coordinates = $0.components(separatedBy: ",") + guard coordinates.count == 2, + let latitudeString = coordinates.last, + let longitudeString = coordinates.first, + let latitude = LocationDegrees(latitudeString), + let longitude = LocationDegrees(longitudeString) + else { + return nil + } + return Waypoint(coordinate: .init( + latitude: latitude, + longitude: longitude + )) + } + + guard waypoints.count >= 2 else { + return nil + } + + let profileIdentifier = ProfileIdentifier( + rawValue: url.pathComponents.dropLast().suffix(2) + .joined(separator: "/") + ) + + self.init( + waypoints: waypoints, + profileIdentifier: profileIdentifier, + queryItems: URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems + ) + + // Distinguish between Directions API and Map Matching API URLs. + guard url.pathComponents.dropLast().joined(separator: "/").hasSuffix(abridgedPath) else { + return nil + } + } + + private enum CodingKeys: String, CodingKey { + case waypoints + case profileIdentifier = "profile" + case includesSteps = "steps" + case shapeFormat = "geometries" + case routeShapeResolution = "overview" + case attributeOptions = "annotations" + case locale = "language" + case includesSpokenInstructions = "voice_instructions" + case distanceMeasurementSystem = "voice_units" + case includesVisualInstructions = "banner_instructions" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(waypoints, forKey: .waypoints) + try container.encode(profileIdentifier, forKey: .profileIdentifier) + try container.encode(includesSteps, forKey: .includesSteps) + try container.encode(shapeFormat, forKey: .shapeFormat) + try container.encode(routeShapeResolution, forKey: .routeShapeResolution) + try container.encode(attributeOptions, forKey: .attributeOptions) + try container.encode(locale.identifier, forKey: .locale) + try container.encode(includesSpokenInstructions, forKey: .includesSpokenInstructions) + try container.encode(distanceMeasurementSystem, forKey: .distanceMeasurementSystem) + try container.encode(includesVisualInstructions, forKey: .includesVisualInstructions) + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.waypoints = try container.decode([Waypoint].self, forKey: .waypoints) + self.profileIdentifier = try container.decode(ProfileIdentifier.self, forKey: .profileIdentifier) + self.includesSteps = try container.decode(Bool.self, forKey: .includesSteps) + self.shapeFormat = try container.decode(RouteShapeFormat.self, forKey: .shapeFormat) + self.routeShapeResolution = try container.decode(RouteShapeResolution.self, forKey: .routeShapeResolution) + self.attributeOptions = try container.decode(AttributeOptions.self, forKey: .attributeOptions) + let identifier = try container.decode(String.self, forKey: .locale) + self.locale = Locale(identifier: identifier) + self.includesSpokenInstructions = try container.decode(Bool.self, forKey: .includesSpokenInstructions) + self.distanceMeasurementSystem = try container.decode( + MeasurementSystem.self, + forKey: .distanceMeasurementSystem + ) + self.includesVisualInstructions = try container.decode(Bool.self, forKey: .includesVisualInstructions) + } + + // MARK: Specifying the Path of the Route + + /// An array of ``Waypoint`` objects representing locations that the route should visit in chronological order. + /// + /// A waypoint object indicates a location to visit, as well as an optional heading from which to approach the + /// location. + /// The array should contain at least two waypoints(the source and destination) and at most 25 waypoints. + public var waypoints: [Waypoint] + + /// The waypoints that separate legs. + var legSeparators: [Waypoint] { + var waypoints = waypoints + guard waypoints.count > 1 else { return [] } + + let source = waypoints.removeFirst() + let destination = waypoints.removeLast() + return [source] + waypoints.filter(\.separatesLegs) + [destination] + } + + // MARK: Specifying the Mode of Transportation + + /// A string specifying the primary mode of transportation for the routes. + /// + /// The default value of this property is ``ProfileIdentifier/automobile``, which specifies driving directions. + public var profileIdentifier: ProfileIdentifier + + // MARK: Specifying the Response Format + + /// A Boolean value indicating whether ``RouteStep`` objects should be included in the response. + /// + /// If the value of this property is `true`, the returned route contains turn-by-turn instructions. Each returned + /// ``Route`` object contains one or more ``RouteLeg`` object that in turn contains one or more ``RouteStep`` + /// objects. On the other hand, if the value of this property is `false`, the ``RouteLeg`` objects contain no + /// ``RouteStep`` objects. + /// + /// If you only want to know the distance or estimated travel time to a destination, set this property to `false` to + /// minimize the size of the response and the time it takes to calculate the response. If you need to display + /// turn-by-turn instructions, set this property to `true`. + /// + /// The default value of this property is `false`. + public var includesSteps = false + + /// Format of the data from which the shapes of the returned route and its steps are derived. + /// + /// This property has no effect on the returned shape objects, although the choice of format can significantly + /// affect the size of the underlying HTTP response. + /// + /// The default value of this property is ``RouteShapeFormat/polyline``. + public var shapeFormat = RouteShapeFormat.polyline + + /// Resolution of the shape of the returned route. + /// + /// This property has no effect on the shape of the returned route’s steps. + /// + /// The default value of this property is ``RouteShapeResolution/low``, specifying a low-resolution route shape. + public var routeShapeResolution = RouteShapeResolution.low + + /// AttributeOptions for the route. Any combination of ``AttributeOptions`` can be specified. + /// + /// By default, no attribute options are specified. It is recommended that ``routeShapeResolution`` be set to + /// ``RouteShapeResolution/full``. + public var attributeOptions: AttributeOptions = [] + + /// The locale in which the route’s instructions are written. + /// + /// If you use the MapboxDirections framework with the Mapbox Directions API or Map Matching API, this property + /// affects the sentence contained within the ``RouteStep/instructions`` property, but it does not affect any road + /// names contained in that property or other properties such as ``RouteStep/names``. + /// + /// The Directions API can provide instructions in [a number of + /// languages](https://docs.mapbox.com/api/navigation/#instructions-languages). Set this property to + /// `Bundle.main.preferredLocalizations.first` or `Locale.autoupdatingCurrent` to match the application’s language + /// or the system language, respectively. + /// + /// By default, this property is set to the current system locale. + public var locale = Locale.current { + didSet { + distanceMeasurementSystem = locale.usesMetricSystem ? .metric : .imperial + } + } + + /// A Boolean value indicating whether each route step includes an array of ``SpokenInstruction``. + /// + /// If this option is set to true, the ``RouteStep/instructionsSpokenAlongStep`` property is set to an array of + /// ``SpokenInstruction``. + public var includesSpokenInstructions = false + + /// The measurement system used in spoken instructions included in route steps. + /// + /// If the ``includesSpokenInstructions`` property is set to `true`, this property determines the units used for + /// measuring the distance remaining until an upcoming maneuver. If the ``includesSpokenInstructions`` property is + /// set to `false`, this property has no effect. + /// + /// You should choose a measurement system appropriate for the current region. You can also allow the user to + /// indicate their preferred measurement system via a setting. + public var distanceMeasurementSystem: MeasurementSystem = Locale.current.usesMetricSystem ? .metric : .imperial + + /// If true, each ``RouteStep`` will contain the property ``RouteStep/instructionsDisplayedAlongStep``. + /// + /// ``RouteStep/instructionsDisplayedAlongStep`` contains an array of ``VisualInstruction`` objects used for + /// visually conveying + /// information about a given ``RouteStep``. + public var includesVisualInstructions = false + + /// The time immediately before a `Directions` object fetched this result. + /// + /// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to + /// `nil`; use the `URLSessionTaskTransactionMetrics.fetchStartDate` property instead. This property may also be set + /// to `nil` if you create this result from a JSON object or encoded object. + /// + /// This property does not persist after encoding and decoding. + public var fetchStartDate: Date? + + // MARK: Getting the Request URL + + /// The path of the request URL, specifying service name, version and profile. + /// + /// The query items are included in the URL of a GET request or the body of a POST request. + var abridgedPath: String { + assertionFailure("abridgedPath should be overriden by subclass") + return "" + } + + /// The path of the request URL, not including the hostname or any parameters. + var path: String { + guard let coordinates else { + assertionFailure("No query") + return "" + } + + if waypoints.count < 2 { + return "\(abridgedPath)" + } + + return "\(abridgedPath)/\(coordinates)" + } + + /// An array of URL query items (parameters) to include in an HTTP request. + /// + /// The query items are included in the URL of a GET request or the body of a POST request. + public var urlQueryItems: [URLQueryItem] { + var queryItems: [URLQueryItem] = [ + URLQueryItem(name: "geometries", value: shapeFormat.rawValue), + URLQueryItem(name: "overview", value: routeShapeResolution.rawValue), + + URLQueryItem(name: "steps", value: String(includesSteps)), + URLQueryItem(name: "language", value: locale.identifier), + ] + + let mustArriveOnDrivingSide = !waypoints.filter { !$0.allowsArrivingOnOppositeSide }.isEmpty + if mustArriveOnDrivingSide { + let approaches = waypoints.map { $0.allowsArrivingOnOppositeSide ? "unrestricted" : "curb" } + queryItems.append(URLQueryItem(name: "approaches", value: approaches.joined(separator: ";"))) + } + + if includesSpokenInstructions { + queryItems.append(URLQueryItem(name: "voice_instructions", value: String(includesSpokenInstructions))) + queryItems.append(URLQueryItem(name: "voice_units", value: distanceMeasurementSystem.rawValue)) + } + + if includesVisualInstructions { + queryItems.append(URLQueryItem(name: "banner_instructions", value: String(includesVisualInstructions))) + } + + // Include headings and heading accuracies if any waypoint has a nonnegative heading. + if let bearings { + queryItems.append(URLQueryItem(name: "bearings", value: bearings)) + } + + // Include location accuracies if any waypoint has a nonnegative coordinate accuracy. + if let radiuses { + queryItems.append(URLQueryItem(name: "radiuses", value: radiuses)) + } + + if let annotations { + queryItems.append(URLQueryItem(name: "annotations", value: annotations)) + } + + if let waypointIndices { + queryItems.append(URLQueryItem(name: "waypoints", value: waypointIndices)) + } + + if let names = waypointNames { + queryItems.append(URLQueryItem(name: "waypoint_names", value: names)) + } + + if let snapping = closureSnapping { + queryItems.append(URLQueryItem(name: "snapping_include_closures", value: snapping)) + } + + if let staticClosureSnapping { + queryItems.append(URLQueryItem(name: "snapping_include_static_closures", value: staticClosureSnapping)) + } + + return queryItems + } + + var bearings: String? { + guard waypoints.contains(where: { $0.heading ?? -1 >= 0 }) else { + return nil + } + return waypoints.map(\.headingDescription).joined(separator: ";") + } + + var radiuses: String? { + guard waypoints.contains(where: { $0.coordinateAccuracy ?? -1 >= 0 }) else { + return nil + } + + let accuracies = waypoints.map { waypoint -> String in + guard let accuracy = waypoint.coordinateAccuracy, accuracy >= 0 else { + return "unlimited" + } + return String(accuracy) + } + return accuracies.joined(separator: ";") + } + + private var approaches: String? { + if waypoints.filter({ !$0.allowsArrivingOnOppositeSide }).isEmpty { + return nil + } + return waypoints.map { $0.allowsArrivingOnOppositeSide ? "unrestricted" : "curb" }.joined(separator: ";") + } + + private var annotations: String? { + if attributeOptions.isEmpty { + return nil + } + return attributeOptions.description + } + + private var waypointIndices: String? { + var waypointIndices = waypoints.indices { $0.separatesLegs } + waypointIndices.insert(waypoints.startIndex) + waypointIndices.insert(waypoints.endIndex - 1) + + guard waypointIndices.count < waypoints.count else { + return nil + } + return waypointIndices.map(String.init(describing:)).joined(separator: ";") + } + + private var waypointNames: String? { + guard !waypoints.compactMap(\.name).isEmpty, waypoints.count > 1 else { + return nil + } + return legSeparators.map { $0.name ?? "" }.joined(separator: ";") + } + + var coordinates: String? { + return waypoints.map(\.coordinate.requestDescription).joined(separator: ";") + } + + var closureSnapping: String? { + makeStringFromBoolProperties(of: waypoints, for: \.allowsSnappingToClosedRoad) + } + + var staticClosureSnapping: String? { + makeStringFromBoolProperties(of: waypoints, for: \.allowsSnappingToStaticallyClosedRoad) + } + + private func makeStringFromBoolProperties(of elements: [T], for keyPath: KeyPath) -> String? { + guard elements.contains(where: { $0[keyPath: keyPath] }) else { return nil } + return elements.map { $0[keyPath: keyPath] ? "true" : "" }.joined(separator: ";") + } + + var httpBody: String { + guard let coordinates else { return "" } + var components = URLComponents() + components.queryItems = urlQueryItems + [ + URLQueryItem(name: "coordinates", value: coordinates), + ] + return components.percentEncodedQuery ?? "" + } +} + +extension DirectionsOptions: Equatable { + public static func == (lhs: DirectionsOptions, rhs: DirectionsOptions) -> Bool { + return lhs.waypoints == rhs.waypoints && + lhs.profileIdentifier == rhs.profileIdentifier && + lhs.includesSteps == rhs.includesSteps && + lhs.shapeFormat == rhs.shapeFormat && + lhs.routeShapeResolution == rhs.routeShapeResolution && + lhs.attributeOptions == rhs.attributeOptions && + lhs.locale.identifier == rhs.locale.identifier && + lhs.includesSpokenInstructions == rhs.includesSpokenInstructions && + lhs.distanceMeasurementSystem == rhs.distanceMeasurementSystem && + lhs.includesVisualInstructions == rhs.includesVisualInstructions + } +} + +@available(*, unavailable) +extension DirectionsOptions: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxDirections/DirectionsResult.swift b/ios/Classes/Navigation/MapboxDirections/DirectionsResult.swift new file mode 100644 index 000000000..2ef311f70 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/DirectionsResult.swift @@ -0,0 +1,245 @@ +import Foundation +import Turf + +public enum DirectionsResultCodingKeys: String, CodingKey, CaseIterable { + case shape = "geometry" + case legs + case distance + case expectedTravelTime = "duration" + case typicalTravelTime = "duration_typical" + case directionsOptions + case speechLocale = "voiceLocale" +} + +public struct DirectionsCodingKey: CodingKey { + public var intValue: Int? { nil } + public init?(intValue: Int) { + nil + } + + public let stringValue: String + public init(stringValue: String) { + self.stringValue = stringValue + } + + public static func directionsResult(_ key: DirectionsResultCodingKeys) -> Self { + .init(stringValue: key.rawValue) + } +} + +/// A `DirectionsResult` represents a result returned from either the Mapbox Directions service. +/// +/// You do not create instances of this class directly. Instead, you receive ``Route`` or ``Match`` objects when you +/// request directions using the `Directions.calculate(_:completionHandler:)` or +/// `Directions.calculateRoutes(matching:completionHandler:)` method. +public protocol DirectionsResult: Codable, ForeignMemberContainer, Equatable, Sendable { + // MARK: Getting the Shape of the Route + + /// The roads or paths taken as a contiguous polyline. + /// + /// The shape may be `nil` or simplified depending on the ``DirectionsOptions/routeShapeResolution`` property of the + /// original ``RouteOptions`` or ``MatchOptions`` object. + /// + /// Using the [Mapbox Maps SDK for iOS](https://docs.mapbox.com/ios/maps/) or [Mapbox Maps SDK for + /// macOS](https://mapbox.github.io/mapbox-gl-native/macos/), you can create an `MGLPolyline` object using these + /// coordinates to display an overview of the route on an `MGLMapView`. + var shape: LineString? { get } + + // MARK: Getting the Legs Along the Route + + /// The legs that are traversed in order. + /// + /// The number of legs in this array depends on the number of waypoints. A route with two waypoints (the source and + /// destination) has one leg, a route with three waypoints (the source, an intermediate waypoint, and the + /// destination) has two legs, and so on. + /// + /// To determine the name of the route, concatenate the names of the route’s legs. + var legs: [RouteLeg] { get set } + + // MARK: Getting Statistics About the Route + + /// The route’s distance, measured in meters. + /// + /// The value of this property accounts for the distance that the user must travel to traverse the path of the + /// route. It is the sum of the ``RouteLeg/distance`` properties of the route’s legs, not the sum of the direct + /// distances between the route’s waypoints. You should not assume that the user would travel along this distance at + /// a fixed speed. + var distance: Turf.LocationDistance { get } + + /// The route’s expected travel time, measured in seconds. + /// + /// The value of this property reflects the time it takes to traverse the entire route. It is the sum of the + /// ``expectedTravelTime`` properties of the route’s legs. If the route was calculated using the + /// ``ProfileIdentifier/automobileAvoidingTraffic`` profile, this property reflects current traffic conditions at + /// the time of the request, not necessarily the traffic conditions at the time the user would begin the route. For + /// other profiles, this property reflects travel time under ideal conditions and does not account for traffic + /// congestion. If the route makes use of a ferry or train, the actual travel time may additionally be subject to + /// the schedules of those services. + /// + /// Do not assume that the user would travel along the route at a fixed speed. For more granular travel times, use + /// the ``RouteLeg/expectedTravelTime`` or ``RouteStep/expectedTravelTime``. For even more granularity, specify the + /// ``AttributeOptions/expectedTravelTime`` option and use the ``RouteLeg/expectedSegmentTravelTimes`` property. + var expectedTravelTime: TimeInterval { get set } + + /// The route’s typical travel time, measured in seconds. + /// + /// The value of this property reflects the typical time it takes to traverse the entire route. It is the sum of the + /// ``typicalTravelTime`` properties of the route’s legs. This property is available when using the + /// ``ProfileIdentifier/automobileAvoidingTraffic`` profile. This property reflects typical traffic conditions at + /// the time of the request, not necessarily the typical traffic conditions at the time the user would begin the + /// route. If the route makes use of a ferry, the typical travel time may additionally be subject to the schedule of + /// this service. + /// + /// Do not assume that the user would travel along the route at a fixed speed. For more granular typical travel + /// times, use the ``RouteLeg/typicalTravelTime`` or ``RouteStep/typicalTravelTime``. + var typicalTravelTime: TimeInterval? { get set } + + // MARK: Configuring Speech Synthesis + + /// The locale to use for spoken instructions. + /// + /// This locale is specific to Mapbox Voice API. If `nil` is returned, the instruction should be spoken with an + /// alternative speech synthesizer. + var speechLocale: Locale? { get set } + + // MARK: Auditing the Server Response + + /// The time immediately before a `Directions` object fetched this result. + /// + /// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to + /// `nil`; use the `URLSessionTaskTransactionMetrics.fetchStartDate` property instead. This property may also be set + /// to `nil` if you create this result from a JSON object or encoded object. + /// + /// This property does not persist after encoding and decoding. + var fetchStartDate: Date? { get set } + + /// The time immediately before a `Directions` object received the last byte of this result. + /// + /// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to + /// `nil`; use the `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be + /// set to `nil` if you create this result from a JSON object or encoded object. + /// + /// This property does not persist after encoding and decoding. + var responseEndDate: Date? { get set } + + /// Internal indicator of whether response contained the ``speechLocale`` entry. + /// + /// Directions API includes ``speechLocale`` if ``DirectionsOptions/includesSpokenInstructions`` option was enabled + /// in the request. + /// + /// This property persists after encoding and decoding. + var responseContainsSpeechLocale: Bool { get } + + var legSeparators: [Waypoint?] { get set } +} + +extension DirectionsResult { + public var legSeparators: [Waypoint?] { + get { + return legs.isEmpty ? [] : ([legs[0].source] + legs.map(\.destination)) + } + set { + let endpointsByLeg = zip(newValue, newValue.suffix(from: 1)) + var legIdx = legs.startIndex + for endpoint in endpointsByLeg where legIdx != legs.endIndex { + legs[legIdx].source = endpoint.0 + legs[legIdx].destination = endpoint.1 + legIdx = legs.index(after: legIdx) + } + } + } + + // MARK: - Decode + + static func decodeLegs( + using container: KeyedDecodingContainer, + options: DirectionsOptions + ) throws -> [RouteLeg] { + var legs = try container.decode([RouteLeg].self, forKey: .directionsResult(.legs)) + legs.populate(waypoints: options.legSeparators) + return legs + } + + static func decodeDistance( + using container: KeyedDecodingContainer + ) throws -> Turf.LocationDistance { + try container.decode(Turf.LocationDistance.self, forKey: .directionsResult(.distance)) + } + + static func decodeExpectedTravelTime( + using container: KeyedDecodingContainer + ) throws -> TimeInterval { + try container.decode(TimeInterval.self, forKey: .directionsResult(.expectedTravelTime)) + } + + static func decodeTypicalTravelTime( + using container: KeyedDecodingContainer + ) throws -> TimeInterval? { + try container.decodeIfPresent(TimeInterval.self, forKey: .directionsResult(.typicalTravelTime)) + } + + static func decodeShape( + using container: KeyedDecodingContainer + ) throws -> LineString? { + try container.decodeIfPresent(PolyLineString.self, forKey: .directionsResult(.shape)) + .map(LineString.init(polyLineString:)) + } + + static func decodeSpeechLocale( + using container: KeyedDecodingContainer + ) throws -> Locale? { + try container.decodeIfPresent(String.self, forKey: .directionsResult(.speechLocale)) + .map(Locale.init(identifier:)) + } + + static func decodeResponseContainsSpeechLocale( + using container: KeyedDecodingContainer + ) throws -> Bool { + container.contains(.directionsResult(.speechLocale)) + } + + // MARK: - Encode + + func encodeLegs( + into container: inout KeyedEncodingContainer + ) throws { + try container.encode(legs, forKey: .directionsResult(.legs)) + } + + func encodeShape( + into container: inout KeyedEncodingContainer, + options: DirectionsOptions? + ) throws { + guard let shape else { return } + + let shapeFormat = options?.shapeFormat ?? .default + let polyLineString = PolyLineString(lineString: shape, shapeFormat: shapeFormat) + try container.encode(polyLineString, forKey: .directionsResult(.shape)) + } + + func encodeDistance( + into container: inout KeyedEncodingContainer + ) throws { + try container.encode(distance, forKey: .directionsResult(.distance)) + } + + func encodeExpectedTravelTime( + into container: inout KeyedEncodingContainer + ) throws { + try container.encode(expectedTravelTime, forKey: .directionsResult(.expectedTravelTime)) + } + + func encodeTypicalTravelTime( + into container: inout KeyedEncodingContainer + ) throws { + try container.encodeIfPresent(typicalTravelTime, forKey: .directionsResult(.typicalTravelTime)) + } + + func encodeSpeechLocale( + into container: inout KeyedEncodingContainer + ) throws { + if responseContainsSpeechLocale { + try container.encode(speechLocale?.identifier, forKey: .directionsResult(.speechLocale)) + } + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/DrivingSide.swift b/ios/Classes/Navigation/MapboxDirections/DrivingSide.swift new file mode 100644 index 000000000..8b42bf5a0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/DrivingSide.swift @@ -0,0 +1,12 @@ +import Foundation + +/// A `DrivingSide` indicates which side of the road cars and traffic flow. +public enum DrivingSide: String, Codable, Equatable, Sendable { + /// Indicates driving occurs on the `left` side. + case left + + /// Indicates driving occurs on the `right` side. + case right + + static let `default` = DrivingSide.right +} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/Array.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/Array.swift new file mode 100644 index 000000000..0cc1a8476 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Extensions/Array.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Collection { + /// Returns an index set containing the indices that satisfy the given predicate. + func indices(where predicate: (Element) throws -> Bool) rethrows -> IndexSet { + return try IndexSet(enumerated().filter { try predicate($0.element) }.map(\.offset)) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/Codable.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/Codable.swift new file mode 100644 index 000000000..cbfe34e5c --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Extensions/Codable.swift @@ -0,0 +1,84 @@ +import Foundation +import Turf + +extension LineString { + /// Returns a string representation of the line string in [Polyline Algorithm + /// Format](https://developers.google.com/maps/documentation/utilities/polylinealgorithm). + func polylineEncodedString(precision: Double = 1e5) -> String { +#if canImport(CoreLocation) + let coordinates = coordinates +#else + let coordinates = self.coordinates.map { Polyline.LocationCoordinate2D( + latitude: $0.latitude, + longitude: $0.longitude + ) } +#endif + return encodeCoordinates(coordinates, precision: precision) + } +} + +enum PolyLineString { + case lineString(_ lineString: LineString) + case polyline(_ encodedPolyline: String, precision: Double) + + init(lineString: LineString, shapeFormat: RouteShapeFormat) { + switch shapeFormat { + case .geoJSON: + self = .lineString(lineString) + case .polyline, .polyline6: + let precision = shapeFormat == .polyline6 ? 1e6 : 1e5 + let encodedPolyline = lineString.polylineEncodedString(precision: precision) + self = .polyline(encodedPolyline, precision: precision) + } + } +} + +extension PolyLineString: Codable { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let options = decoder.userInfo[.options] as? DirectionsOptions + switch options?.shapeFormat ?? .default { + case .geoJSON: + self = try .lineString(container.decode(LineString.self)) + case .polyline, .polyline6: + let precision = options?.shapeFormat == .polyline6 ? 1e6 : 1e5 + let encodedPolyline = try container.decode(String.self) + self = .polyline(encodedPolyline, precision: precision) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .lineString(let lineString): + try container.encode(lineString) + case .polyline(let encodedPolyline, precision: _): + try container.encode(encodedPolyline) + } + } +} + +struct LocationCoordinate2DCodable: Codable { + var latitude: Turf.LocationDegrees + var longitude: Turf.LocationDegrees + var decodedCoordinates: Turf.LocationCoordinate2D { + return Turf.LocationCoordinate2D(latitude: latitude, longitude: longitude) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(longitude) + try container.encode(latitude) + } + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + self.longitude = try container.decode(Turf.LocationDegrees.self) + self.latitude = try container.decode(Turf.LocationDegrees.self) + } + + init(_ coordinate: Turf.LocationCoordinate2D) { + self.latitude = coordinate.latitude + self.longitude = coordinate.longitude + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/CoreLocation.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/CoreLocation.swift new file mode 100644 index 000000000..6e9e7c322 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Extensions/CoreLocation.swift @@ -0,0 +1,31 @@ +import Foundation +#if canImport(CoreLocation) +import CoreLocation +#endif +import Turf + +#if canImport(CoreLocation) +/// The velocity (measured in meters per second) at which the device is moving. +/// +/// This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms +/// that lack Core Location. On Apple platforms, you can use `CLLocationSpeed` anywhere you see this type. +public typealias LocationSpeed = CLLocationSpeed + +/// The accuracy of a geographical coordinate. +/// +/// This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms +/// that lack Core Location. On Apple platforms, you can use `CLLocationAccuracy` anywhere you see this type. +public typealias LocationAccuracy = CLLocationAccuracy +#else +/// The velocity (measured in meters per second) at which the device is moving. +public typealias LocationSpeed = Double + +/// The accuracy of a geographical coordinate. +public typealias LocationAccuracy = Double +#endif + +extension LocationCoordinate2D { + var requestDescription: String { + return "\(longitude.rounded(to: 1e6)),\(latitude.rounded(to: 1e6))" + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/Double.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/Double.swift new file mode 100644 index 000000000..e6783052d --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Extensions/Double.swift @@ -0,0 +1,5 @@ +extension Double { + func rounded(to precision: Double) -> Double { + return (self * precision).rounded() / precision + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/ForeignMemberContainer.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/ForeignMemberContainer.swift new file mode 100644 index 000000000..b47d18489 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Extensions/ForeignMemberContainer.swift @@ -0,0 +1,117 @@ +import Foundation +import Turf + +/// A coding key as an extensible enumeration. +struct AnyCodingKey: CodingKey { + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + init?(intValue: Int) { + self.stringValue = String(intValue) + self.intValue = intValue + } +} + +extension ForeignMemberContainer { + /// Decodes any foreign members using the given decoder. + mutating func decodeForeignMembers( + notKeyedBy _: WellKnownCodingKeys.Type, + with decoder: Decoder + ) throws where WellKnownCodingKeys: CodingKey { + guard (decoder.userInfo[.includesForeignMembers] as? Bool) == true else { return } + + let foreignMemberContainer = try decoder.container(keyedBy: AnyCodingKey.self) + for key in foreignMemberContainer.allKeys { + if WellKnownCodingKeys(stringValue: key.stringValue) == nil { + foreignMembers[key.stringValue] = try foreignMemberContainer.decode(JSONValue?.self, forKey: key) + } + } + } + + /// Encodes any foreign members using the given encoder. + func encodeForeignMembers(notKeyedBy _: WellKnownCodingKeys.Type, to encoder: Encoder) throws + where WellKnownCodingKeys: CodingKey { + guard (encoder.userInfo[.includesForeignMembers] as? Bool) == true else { return } + + var foreignMemberContainer = encoder.container(keyedBy: AnyCodingKey.self) + for (key, value) in foreignMembers { + if let key = AnyCodingKey(stringValue: key), + WellKnownCodingKeys(stringValue: key.stringValue) == nil + { + try foreignMemberContainer.encode(value, forKey: key) + } + } + } +} + +/// A class that can contain foreign members in arbitrary keys. +/// +/// When subclassing ``ForeignMemberContainerClass`` type, you should call +/// ``ForeignMemberContainerClass/decodeForeignMembers(notKeyedBy:with:)`` during your `Decodable.init(from:)` +/// initializer if your subclass has added any new properties. +/// +/// Structures should conform to the `ForeignMemberContainer` protocol instead of this protocol. +public protocol ForeignMemberContainerClass: AnyObject { + /// Foreign members to round-trip to JSON. + /// + /// Foreign members are unrecognized properties, similar to [foreign + /// members](https://datatracker.ietf.org/doc/html/rfc7946#section-6.1) in GeoJSON. This library does not officially + /// support any property that is documented as a “beta” property in the Mapbox Directions API response format, but + /// you can get and set it as an element of this `JSONObject`. + /// + /// Members are coded only if used `JSONEncoder` or `JSONDecoder` has `userInfo[.includesForeignMembers] = true`. + var foreignMembers: JSONObject { get set } + + /// Decodes any foreign members using the given decoder. + /// - Parameters: + /// - codingKeys: `CodingKeys` type which describes all properties declared in current subclass. + /// - decoder: `Decoder` instance, which performs the decoding process. + func decodeForeignMembers( + notKeyedBy codingKeys: WellKnownCodingKeys.Type, + with decoder: Decoder + ) throws where WellKnownCodingKeys: CodingKey & CaseIterable + + /// Encodes any foreign members using the given encoder. + /// + /// This method should be called in your `Encodable.encode(to:)` implementation only in the **base class**. + /// Otherwise it will not encode ``foreignMembers`` or way overwrite it. + /// - Parameter encoder: `Encoder` instance, performing the encoding process. + func encodeForeignMembers(to encoder: Encoder) throws +} + +extension ForeignMemberContainerClass { + public func decodeForeignMembers( + notKeyedBy _: WellKnownCodingKeys.Type, + with decoder: Decoder + ) throws where WellKnownCodingKeys: CodingKey & CaseIterable { + guard (decoder.userInfo[.includesForeignMembers] as? Bool) == true else { return } + + if foreignMembers.isEmpty { + let foreignMemberContainer = try decoder.container(keyedBy: AnyCodingKey.self) + for key in foreignMemberContainer.allKeys { + if WellKnownCodingKeys(stringValue: key.stringValue) == nil { + foreignMembers[key.stringValue] = try foreignMemberContainer.decode(JSONValue?.self, forKey: key) + } + } + } + WellKnownCodingKeys.allCases.forEach { + foreignMembers.removeValue(forKey: $0.stringValue) + } + } + + public func encodeForeignMembers(to encoder: Encoder) throws { + guard (encoder.userInfo[.includesForeignMembers] as? Bool) == true else { return } + + var foreignMemberContainer = encoder.container(keyedBy: AnyCodingKey.self) + for (key, value) in foreignMembers { + if let key = AnyCodingKey(stringValue: key) { + try foreignMemberContainer.encode(value, forKey: key) + } + } + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/GeoJSON.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/GeoJSON.swift new file mode 100644 index 000000000..3752d5482 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Extensions/GeoJSON.swift @@ -0,0 +1,58 @@ +import Foundation +import Turf + +extension BoundingBox: CustomStringConvertible { + public var description: String { + return "\(southWest.longitude),\(southWest.latitude);\(northEast.longitude),\(northEast.latitude)" + } +} + +extension LineString { + init(polyLineString: PolyLineString) throws { + switch polyLineString { + case .lineString(let lineString): + self = lineString + case .polyline(let encodedPolyline, precision: let precision): + self = try LineString(encodedPolyline: encodedPolyline, precision: precision) + } + } + + init(encodedPolyline: String, precision: Double) throws { + guard var coordinates = decodePolyline( + encodedPolyline, + precision: precision + ) as [LocationCoordinate2D]? else { + throw GeometryError.cannotDecodePolyline(precision: precision) + } + // If the polyline has zero length with both endpoints at the same coordinate, Polyline drops one of the + // coordinates. + // https://github.com/raphaelmor/Polyline/issues/59 + // Duplicate the coordinate to ensure a valid GeoJSON geometry. + if coordinates.count == 1 { + coordinates.append(coordinates[0]) + } +#if canImport(CoreLocation) + self.init(coordinates) +#else + self.init(coordinates.map { Turf.LocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) }) +#endif + } +} + +public enum GeometryError: LocalizedError { + case cannotDecodePolyline(precision: Double) + + public var failureReason: String? { + switch self { + case .cannotDecodePolyline(let precision): + return "Unable to decode the string as a polyline with precision \(precision)" + } + } + + public var recoverySuggestion: String? { + switch self { + case .cannotDecodePolyline: + return "Choose the precision that the string was encoded with." + } + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/HTTPURLResponse.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/HTTPURLResponse.swift new file mode 100644 index 000000000..0237c3060 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Extensions/HTTPURLResponse.swift @@ -0,0 +1,30 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +extension HTTPURLResponse { + var rateLimit: UInt? { + guard let limit = allHeaderFields["X-Rate-Limit-Limit"] as? String else { + return nil + } + return UInt(limit) + } + + var rateLimitInterval: TimeInterval? { + guard let interval = allHeaderFields["X-Rate-Limit-Interval"] as? String else { + return nil + } + return TimeInterval(interval) + } + + var rateLimitResetTime: Date? { + guard let resetTime = allHeaderFields["X-Rate-Limit-Reset"] as? String else { + return nil + } + guard let resetTimeNumber = Double(resetTime) else { + return nil + } + return Date(timeIntervalSince1970: resetTimeNumber) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/Measurement.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/Measurement.swift new file mode 100644 index 000000000..b761edcf1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Extensions/Measurement.swift @@ -0,0 +1,100 @@ +import Foundation + +enum SpeedLimitDescriptor: Equatable { + enum UnitDescriptor: String, Codable { + case milesPerHour = "mph" + case kilometersPerHour = "km/h" + + init?(unit: UnitSpeed) { + switch unit { + case .milesPerHour: + self = .milesPerHour + case .kilometersPerHour: + self = .kilometersPerHour + default: + return nil + } + } + + var describedUnit: UnitSpeed { + switch self { + case .milesPerHour: + return .milesPerHour + case .kilometersPerHour: + return .kilometersPerHour + } + } + } + + enum CodingKeys: String, CodingKey { + case none + case speed + case unknown + case unit + } + + case none + case some(speed: Measurement) + case unknown + + init(speed: Measurement?) { + guard let speed else { + self = .unknown + return + } + + if speed.value.isInfinite { + self = .none + } else { + self = .some(speed: speed) + } + } +} + +extension SpeedLimitDescriptor: Codable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if try (container.decodeIfPresent(Bool.self, forKey: .none)) ?? false { + self = .none + } else if try (container.decodeIfPresent(Bool.self, forKey: .unknown)) ?? false { + self = .unknown + } else { + let unitDescriptor = try container.decode(UnitDescriptor.self, forKey: .unit) + let unit = unitDescriptor.describedUnit + let value = try container.decode(Double.self, forKey: .speed) + self = .some(speed: .init(value: value, unit: unit)) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .none: + try container.encode(true, forKey: .none) + case .some(var speed): + let unitDescriptor = UnitDescriptor(unit: speed.unit) ?? { + speed = speed.converted(to: .kilometersPerHour) + return .kilometersPerHour + }() + try container.encode(unitDescriptor, forKey: .unit) + try container.encode(speed.value, forKey: .speed) + case .unknown: + try container.encode(true, forKey: .unknown) + } + } +} + +extension Measurement where UnitType == UnitSpeed { + init?(speedLimitDescriptor: SpeedLimitDescriptor) { + switch speedLimitDescriptor { + case .none: + self = .init(value: .infinity, unit: .kilometersPerHour) + case .some(let speed): + self = speed + case .unknown: + return nil + } + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/String.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/String.swift new file mode 100644 index 000000000..abdaded63 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Extensions/String.swift @@ -0,0 +1,7 @@ +import Foundation + +extension String { + var nonEmptyString: String? { + return !isEmpty ? self : nil + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/URL+Request.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/URL+Request.swift new file mode 100644 index 000000000..b16165056 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Extensions/URL+Request.swift @@ -0,0 +1,89 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +extension URL { + init(path: String, host: URL) { + guard let url = URL(string: path, relativeTo: host) else { + assertionFailure("Cannot form valid URL from '\(path)' relative to '\(host)'") + self = host + return + } + self = url + } +} + +extension URLRequest { + mutating func setupUserAgentString() { + setValue(userAgent, forHTTPHeaderField: "User-Agent") + } +} + +/// The user agent string for any HTTP requests performed directly within this library. +let userAgent: String = { + var components: [String] = [] + + if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? Bundle.main + .infoDictionary?["CFBundleIdentifier"] as? String + { + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + components.append("\(appName)/\(version)") + } + + let libraryBundle: Bundle? = Bundle(for: Directions.self) + + if let libraryName = libraryBundle?.infoDictionary?["CFBundleName"] as? String, + let version = libraryBundle?.infoDictionary?["CFBundleShortVersionString"] as? String + { + components.append("\(libraryName)/\(version)") + } + + // `ProcessInfo().operatingSystemVersionString` can replace this when swift-corelibs-foundaton is next released: + // https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/ProcessInfo.swift#L104-L202 + let system: String +#if os(macOS) + system = "macOS" +#elseif os(iOS) + system = "iOS" +#elseif os(watchOS) + system = "watchOS" +#elseif os(tvOS) + system = "tvOS" +#elseif os(Linux) + system = "Linux" +#else + system = "unknown" +#endif + let systemVersion = ProcessInfo.processInfo.operatingSystemVersion + components + .append("\(system)/\(systemVersion.majorVersion).\(systemVersion.minorVersion).\(systemVersion.patchVersion)") + + let chip: String +#if arch(x86_64) + chip = "x86_64" +#elseif arch(arm) + chip = "arm" +#elseif arch(arm64) + chip = "arm64" +#elseif arch(i386) + chip = "i386" +#else + // Maybe fall back on `uname(2).machine`? + chip = "unrecognized" +#endif + + var simulator: String? +#if targetEnvironment(simulator) + simulator = "Simulator" +#endif + + let otherComponents = [ + chip, + simulator, + ].compactMap { $0 } + + components.append("(\(otherComponents.joined(separator: "; ")))") + + return components.joined(separator: " ") +}() diff --git a/ios/Classes/Navigation/MapboxDirections/Incident.swift b/ios/Classes/Navigation/MapboxDirections/Incident.swift new file mode 100644 index 000000000..37b859a49 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Incident.swift @@ -0,0 +1,304 @@ +import Foundation +import Turf + +/// `Incident` describes any corresponding event, used for annotating the route. +public struct Incident: Codable, Equatable, ForeignMemberContainer, Sendable { + public var foreignMembers: JSONObject = [:] + public var congestionForeignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey { + case identifier = "id" + case type + case description + case creationDate = "creation_time" + case startDate = "start_time" + case endDate = "end_time" + case impact + case subtype = "sub_type" + case subtypeDescription = "sub_type_description" + case alertCodes = "alertc_codes" + case lanesBlocked = "lanes_blocked" + case geometryIndexStart = "geometry_index_start" + case geometryIndexEnd = "geometry_index_end" + case countryCodeAlpha3 = "iso_3166_1_alpha3" + case countryCode = "iso_3166_1_alpha2" + case roadIsClosed = "closed" + case longDescription = "long_description" + case numberOfBlockedLanes = "num_lanes_blocked" + case congestionLevel = "congestion" + case affectedRoadNames = "affected_road_names" + } + + /// Defines known types of incidents. + /// + /// Each incident may or may not have specific set of data, depending on it's `kind` + public enum Kind: String, Sendable { + /// Accident + case accident + /// Congestion + case congestion + /// Construction + case construction + /// Disabled vehicle + case disabledVehicle = "disabled_vehicle" + /// Lane restriction + case laneRestriction = "lane_restriction" + /// Mass transit + case massTransit = "mass_transit" + /// Miscellaneous + case miscellaneous + /// Other news + case otherNews = "other_news" + /// Planned event + case plannedEvent = "planned_event" + /// Road closure + case roadClosure = "road_closure" + /// Road hazard + case roadHazard = "road_hazard" + /// Weather + case weather + + /// Undefined + case undefined + } + + /// Represents the impact of the incident on local traffic. + public enum Impact: String, Codable, Sendable { + /// Unknown impact + case unknown + /// Critical impact + case critical + /// Major impact + case major + /// Minor impact + case minor + /// Low impact + case low + } + + private struct CongestionContainer: Codable, ForeignMemberContainer, Sendable { + var foreignMembers: JSONObject = [:] + + // `Directions` define this as service value to indicate "no congestion calculated" + // see: https://docs.mapbox.com/api/navigation/directions/#incident-object + private static let CongestionUnavailableKey = 101 + + enum CodingKeys: String, CodingKey { + case value + } + + let value: Int + var clampedValue: Int? { + value == Self.CongestionUnavailableKey ? nil : value + } + + init(value: Int) { + self.value = value + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.value = try container.decode(Int.self, forKey: .value) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(value, forKey: .value) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + } + + /// Incident identifier + public var identifier: String + /// The kind of an incident + /// + /// This value is set to `nil` if ``kind`` value is not supported. + public var kind: Kind? { + return Kind(rawValue: rawKind) + } + + var rawKind: String + /// Short description of an incident. May be used as an additional info. + public var description: String + /// Date when incident item was created. + public var creationDate: Date + /// Date when incident happened. + public var startDate: Date + /// Date when incident shall end. + public var endDate: Date + /// Shows severity of an incident. May be not available for all incident types. + public var impact: Impact? + /// Provides additional classification of an incident. May be not available for all incident types. + public var subtype: String? + /// Breif description of the subtype. May be not available for all incident types and is not available if + /// ``subtype`` is `nil`. + public var subtypeDescription: String? + /// The three-letter ISO 3166-1 alpha-3 code for the country the incident is located in. Example: "USA". + public var countryCodeAlpha3: String? + /// The two-letter ISO 3166-1 alpha-2 code for the country the incident is located in. Example: "US". + public var countryCode: String? + /// If this is true then the road has been completely closed. + public var roadIsClosed: Bool? + /// A long description of the incident in a human-readable format. + public var longDescription: String? + /// The number of items in the ``lanesBlocked``. + public var numberOfBlockedLanes: Int? + /// Information about the amount of congestion on the road around the incident. + /// + /// A number between 0 and 100 representing the level of congestion caused by the incident. The higher the number, + /// the more congestion there is. A value of 0 means there is no congestion on the road. A value of 100 means that + /// the road is closed. + public var congestionLevel: NumericCongestionLevel? + /// List of roads names affected by the incident. + /// + /// Alternate road names are separated by a /. The list is ordered from the first affected road to the last one that + /// the incident lies on. + public var affectedRoadNames: [String]? + /// Contains list of ISO 14819-2:2013 codes + /// + /// See https://www.iso.org/standard/59231.html for details + public var alertCodes: Set + /// A list of lanes, affected by the incident + /// + /// `nil` value indicates that lanes data is not available + public var lanesBlocked: BlockedLanes? + /// The range of segments within the overall leg, where the incident spans. + public var shapeIndexRange: Range + + public init( + identifier: String, + type: Kind, + description: String, + creationDate: Date, + startDate: Date, + endDate: Date, + impact: Impact?, + subtype: String?, + subtypeDescription: String?, + alertCodes: Set, + lanesBlocked: BlockedLanes?, + shapeIndexRange: Range, + countryCodeAlpha3: String? = nil, + countryCode: String? = nil, + roadIsClosed: Bool? = nil, + longDescription: String? = nil, + numberOfBlockedLanes: Int? = nil, + congestionLevel: NumericCongestionLevel? = nil, + affectedRoadNames: [String]? = nil + ) { + self.identifier = identifier + self.rawKind = type.rawValue + self.description = description + self.creationDate = creationDate + self.startDate = startDate + self.endDate = endDate + self.impact = impact + self.subtype = subtype + self.subtypeDescription = subtypeDescription + self.alertCodes = alertCodes + self.lanesBlocked = lanesBlocked + self.shapeIndexRange = shapeIndexRange + self.countryCodeAlpha3 = countryCodeAlpha3 + self.countryCode = countryCode + self.roadIsClosed = roadIsClosed + self.longDescription = longDescription + self.numberOfBlockedLanes = numberOfBlockedLanes + self.congestionLevel = congestionLevel + self.affectedRoadNames = affectedRoadNames + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let formatter = ISO8601DateFormatter() + + self.identifier = try container.decode(String.self, forKey: .identifier) + self.rawKind = try container.decode(String.self, forKey: .type) + + self.description = try container.decode(String.self, forKey: .description) + + if let date = try formatter.date(from: container.decode(String.self, forKey: .creationDate)) { + self.creationDate = date + } else { + throw DecodingError.dataCorruptedError( + forKey: .creationDate, + in: container, + debugDescription: "`Intersection.creationTime` is encoded with invalid format." + ) + } + if let date = try formatter.date(from: container.decode(String.self, forKey: .startDate)) { + self.startDate = date + } else { + throw DecodingError.dataCorruptedError( + forKey: .startDate, + in: container, + debugDescription: "`Intersection.startTime` is encoded with invalid format." + ) + } + if let date = try formatter.date(from: container.decode(String.self, forKey: .endDate)) { + self.endDate = date + } else { + throw DecodingError.dataCorruptedError( + forKey: .endDate, + in: container, + debugDescription: "`Intersection.endTime` is encoded with invalid format." + ) + } + + self.impact = try container.decodeIfPresent(Impact.self, forKey: .impact) + self.subtype = try container.decodeIfPresent(String.self, forKey: .subtype) + self.subtypeDescription = try container.decodeIfPresent(String.self, forKey: .subtypeDescription) + self.alertCodes = try container.decode(Set.self, forKey: .alertCodes) + + self.lanesBlocked = try container.decodeIfPresent(BlockedLanes.self, forKey: .lanesBlocked) + + let geometryIndexStart = try container.decode(Int.self, forKey: .geometryIndexStart) + let geometryIndexEnd = try container.decode(Int.self, forKey: .geometryIndexEnd) + self.shapeIndexRange = geometryIndexStart.. 1 { + let context = EncodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Inconsistent valid indications." + ) + throw EncodingError.invalidValue(validIndications, context) + } + self.usableLaneIndication = validIndications.first + } else { + self.approachLanes = nil + self.usableApproachLanes = nil + self.preferredApproachLanes = nil + self.usableLaneIndication = nil + } + + self.outletRoadClasses = try container.decodeIfPresent(RoadClasses.self, forKey: .outletRoadClasses) + + let outletsArray = try container.decode([Bool].self, forKey: .outletIndexes) + self.outletIndexes = outletsArray.indices { $0 } + + self.outletIndex = try container.decodeIfPresent(Int.self, forKey: .outletIndex) + self.approachIndex = try container.decodeIfPresent(Int.self, forKey: .approachIndex) + + self.tollCollection = try container.decodeIfPresent(TollCollection.self, forKey: .tollCollection) + + self.tunnelName = try container.decodeIfPresent(String.self, forKey: .tunnelName) + + self.outletMapboxStreetsRoadClass = try container.decodeIfPresent( + MapboxStreetClassCodable.self, + forKey: .mapboxStreets + )?.streetClass + + self.isUrban = try container.decodeIfPresent(Bool.self, forKey: .isUrban) + + self.restStop = try container.decodeIfPresent(RestStop.self, forKey: .restStop) + + self.railroadCrossing = try container.decodeIfPresent(Bool.self, forKey: .railroadCrossing) + self.trafficSignal = try container.decodeIfPresent(Bool.self, forKey: .trafficSignal) + self.stopSign = try container.decodeIfPresent(Bool.self, forKey: .stopSign) + self.yieldSign = try container.decodeIfPresent(Bool.self, forKey: .yieldSign) + + self.interchange = try container.decodeIfPresent(Interchange.self, forKey: .interchange) + self.junction = try container.decodeIfPresent(Junction.self, forKey: .junction) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } +} + +extension Intersection { + public static func == (lhs: Intersection, rhs: Intersection) -> Bool { + return lhs.location == rhs.location && + lhs.headings == rhs.headings && + lhs.outletIndexes == rhs.outletIndexes && + lhs.approachIndex == rhs.approachIndex && + lhs.outletIndex == rhs.outletIndex && + lhs.approachLanes == rhs.approachLanes && + lhs.usableApproachLanes == rhs.usableApproachLanes && + lhs.preferredApproachLanes == rhs.preferredApproachLanes && + lhs.usableLaneIndication == rhs.usableLaneIndication && + lhs.restStop == rhs.restStop && + lhs.regionCode == rhs.regionCode && + lhs.outletMapboxStreetsRoadClass == rhs.outletMapboxStreetsRoadClass && + lhs.outletRoadClasses == rhs.outletRoadClasses && + lhs.tollCollection == rhs.tollCollection && + lhs.tunnelName == rhs.tunnelName && + lhs.isUrban == rhs.isUrban && + lhs.railroadCrossing == rhs.railroadCrossing && + lhs.trafficSignal == rhs.trafficSignal && + lhs.stopSign == rhs.stopSign && + lhs.yieldSign == rhs.yieldSign && + lhs.interchange == rhs.interchange && + lhs.junction == rhs.junction + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/IsochroneError.swift b/ios/Classes/Navigation/MapboxDirections/IsochroneError.swift new file mode 100644 index 000000000..b7698e189 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/IsochroneError.swift @@ -0,0 +1,61 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +/// An error that occurs when calculating isochrone contours. +public enum IsochroneError: LocalizedError { + public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { + if let response = response as? HTTPURLResponse { + switch (response.statusCode, code ?? "") { + case (200, "NoSegment"): + self = .unableToLocate + case (404, "ProfileNotFound"): + self = .profileNotFound + case (422, "InvalidInput"): + self = .invalidInput(message: message) + case (429, _): + self = .rateLimited( + rateLimitInterval: response.rateLimitInterval, + rateLimit: response.rateLimit, + resetTime: response.rateLimitResetTime + ) + default: + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } else { + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } + + /// There is no network connection available to perform the network request. + case network(_: URLError) + + /// The server returned a response that isn’t correctly formatted. + case invalidResponse(_: URLResponse?) + + /// The server returned an empty response. + case noData + + /// A specified location could not be associated with a roadway or pathway. + /// + /// Make sure the locations are close enough to a roadway or pathway. + case unableToLocate + + /// Unrecognized profile identifier. + /// + /// Make sure the ``IsochroneOptions/profileIdentifier`` option is set to one of the predefined values, such as + /// ``ProfileIdentifier/automobile``. + case profileNotFound + + /// The API recieved input that it didn't understand. + case invalidInput(message: String?) + + /// Too many requests have been made with the same access token within a certain period of time. + /// + /// Wait before retrying. + case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) + + /// Unknown error case. Look at associated values for more details. + case unknown(response: URLResponse?, underlying: Error?, code: String?, message: String?) +} diff --git a/ios/Classes/Navigation/MapboxDirections/IsochroneOptions.swift b/ios/Classes/Navigation/MapboxDirections/IsochroneOptions.swift new file mode 100644 index 000000000..29b828d43 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/IsochroneOptions.swift @@ -0,0 +1,272 @@ +import Foundation +import Turf + +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif +#if canImport(CoreLocation) +import CoreLocation +#endif + +/// Options for calculating contours from the Mapbox Isochrone service. +public struct IsochroneOptions: Equatable, Sendable { + public init( + centerCoordinate: LocationCoordinate2D, + contours: Contours, + profileIdentifier: ProfileIdentifier = .automobile + ) { + self.centerCoordinate = centerCoordinate + self.contours = contours + self.profileIdentifier = profileIdentifier + } + + // MARK: Configuring the Contour + + /// Contours GeoJSON format. + public enum ContourFormat: Equatable, Sendable { + /// Requested contour will be presented as GeoJSON LineString. + case lineString + /// Requested contour will be presented as GeoJSON Polygon. + case polygon + } + + /// A string specifying the primary mode of transportation for the contours. + /// + /// The default value of this property is ``ProfileIdentifier/automobile``, which specifies driving directions. + public var profileIdentifier: ProfileIdentifier + /// A coordinate around which to center the isochrone lines. + public var centerCoordinate: LocationCoordinate2D + /// Contours bounds and color sheme definition. + public var contours: Contours + + /// Specifies the format of output contours. + /// + /// Defaults to ``ContourFormat/lineString`` which represents contours as linestrings. + public var contoursFormat: ContourFormat = .lineString + + /// Removes contours which are ``denoisingFactor`` times smaller than the biggest one. + /// + /// The default is 1.0. A value of 1.0 will only return the largest contour for a given value. A value of 0.5 drops + /// any contours that are less than half the area of the largest contour in the set of contours for that same value. + public var denoisingFactor: Float? + + /// Douglas-Peucker simplification tolerance. + /// + /// Higher means simpler geometries and faster performance. There is no upper bound. If no value is specified in the + /// request, the Isochrone API will choose the most optimized value to use for the request. + /// + /// - Note: Simplification of contours can lead to self-intersections, as well as intersections of adjacent + /// contours. + public var simplificationTolerance: LocationDistance? + + // MARK: Getting the Request URL + + /// The path of the request URL, specifying service name, version and profile. + var abridgedPath: String { + return "isochrone/v1/\(profileIdentifier.rawValue)" + } + + /// The path of the request URL, not including the hostname or any parameters. + var path: String { + return "\(abridgedPath)/\(centerCoordinate.requestDescription)" + } + + /// An array of URL query items (parameters) to include in an HTTP request. + public var urlQueryItems: [URLQueryItem] { + var queryItems: [URLQueryItem] = [] + + switch contours { + case .byDistances(let definitions): + let fallbackColor = definitions.allSatisfy { $0.color != nil } ? nil : Color.fallbackColor + + queryItems.append(URLQueryItem( + name: "contours_meters", + value: definitions.map { $0.queryValueDescription(roundingTo: .meters) } + .joined(separator: ",") + )) + + let colors = definitions.compactMap { $0.queryColorDescription(fallbackColor: fallbackColor) } + .joined(separator: ",") + if !colors.isEmpty { + queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) + } + case .byExpectedTravelTimes(let definitions): + let fallbackColor = definitions.allSatisfy { $0.color != nil } ? nil : Color.fallbackColor + + queryItems.append(URLQueryItem( + name: "contours_minutes", + value: definitions.map { $0.queryValueDescription(roundingTo: .minutes) } + .joined(separator: ",") + )) + + let colors = definitions.compactMap { $0.queryColorDescription(fallbackColor: fallbackColor) } + .joined(separator: ",") + if !colors.isEmpty { + queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) + } + } + + if contoursFormat == .polygon { + queryItems.append(URLQueryItem(name: "polygons", value: "true")) + } + + if let denoise = denoisingFactor { + queryItems.append(URLQueryItem(name: "denoise", value: String(denoise))) + } + + if let tolerance = simplificationTolerance { + queryItems.append(URLQueryItem(name: "generalize", value: String(tolerance))) + } + + return queryItems + } +} + +extension IsochroneOptions { + /// Definition of contours limits. + public enum Contours: Equatable, Sendable { + /// Describes Individual contour bound and color. + public struct Definition: Equatable, Sendable { + /// Bound measurement value. + public var value: Measurement + /// Contour fill color. + /// + /// If this property is unspecified, the contour is colored gray. If this property is not specified for any + /// contour, the contours are rainbow-colored. + public var color: Color? + + /// Initializes new contour Definition. + public init(value: Measurement, color: Color? = nil) { + self.value = value + self.color = color + } + + /// Initializes new contour Definition. + /// + /// Convenience initializer for encapsulating `Measurement` initialization. + public init(value: Double, unit: Unt, color: Color? = nil) { + self.init( + value: Measurement(value: value, unit: unit), + color: color + ) + } + + func queryValueDescription(roundingTo unit: Unt) -> String { + return String(Int(value.converted(to: unit).value.rounded())) + } + + func queryColorDescription(fallbackColor: Color?) -> String? { + return (color ?? fallbackColor)?.queryDescription + } + } + + /// The desired travel times to use for each isochrone contour. + /// + /// This value will be rounded to minutes. + case byExpectedTravelTimes([Definition]) + + /// The distances to use for each isochrone contour. + /// + /// Will be rounded to meters. + case byDistances([Definition]) + } +} + +extension IsochroneOptions { +#if canImport(UIKit) + /// RGB-based color representation for Isochrone contour. + public typealias Color = UIColor +#elseif canImport(AppKit) + /// RGB-based color representation for Isochrone contour. + public typealias Color = NSColor +#else + /// sRGB color space representation for Isochrone contour. + /// + /// This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple + /// platforms that lack `UIKit` or `AppKit`. On Apple platforms, you can use `UIColor` or `NSColor` respectively + /// anywhere you see this type. + public struct Color { + /// Red color component. + /// + /// Value ranged from `0` up to `255`. + public var red: Int + /// Green color component. + /// + /// Value ranged from `0` up to `255`. + public var green: Int + /// Blue color component. + /// + /// Value ranged from `0` up to `255`. + public var blue: Int + + /// Creates new `Color` instance. + public init(red: Int, green: Int, blue: Int) { + self.red = red + self.green = green + self.blue = blue + } + } +#endif +} + +extension IsochroneOptions.Color { + var queryDescription: String { + let hexFormat = "%02X%02X%02X" + +#if canImport(UIKit) + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + + getRed( + &red, + green: &green, + blue: &blue, + alpha: nil + ) + + return String( + format: hexFormat, + Int(red * 255), + Int(green * 255), + Int(blue * 255) + ) +#elseif canImport(AppKit) + var convertedColor = self + if colorSpace != .sRGB { + guard let converted = usingColorSpace(.sRGB) else { + assertionFailure("Failed to convert Isochrone contour color to RGB space.") + return "000000" + } + + convertedColor = converted + } + + return String( + format: hexFormat, + Int(convertedColor.redComponent * 255), + Int(convertedColor.greenComponent * 255), + Int(convertedColor.blueComponent * 255) + ) +#else + return String( + format: hexFormat, + red, + green, + blue + ) +#endif + } + + static var fallbackColor: IsochroneOptions.Color { +#if canImport(UIKit) + return gray +#elseif canImport(AppKit) + return gray +#else + return IsochroneOptions.Color(red: 128, green: 128, blue: 128) +#endif + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Isochrones.swift b/ios/Classes/Navigation/MapboxDirections/Isochrones.swift new file mode 100644 index 000000000..543dccbd7 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Isochrones.swift @@ -0,0 +1,168 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +#if canImport(CoreLocation) +import CoreLocation +#endif +import Turf + +/// Computes areas that are reachable within a specified amount of time or distance from a location, and returns the +/// reachable regions as contours of polygons or lines that you can display on a map. +open class Isochrones: @unchecked Sendable { + /// A tuple type representing the isochrone session that was generated from the request. + /// - Parameter options: A ``IsochroneOptions`` object representing the request parameter options. + /// - Parameter credentials: A object containing the credentials used to make the request. + public typealias Session = (options: IsochroneOptions, credentials: Credentials) + + /// A closure (block) to be called when a isochrone request is complete. + /// + /// - Parameter result: A `Result` enum that represents the `FeatureCollection` if the request returned + /// successfully, or the error if it did not. + public typealias IsochroneCompletionHandler = @MainActor @Sendable ( + _ result: Result + ) -> Void + + // MARK: Creating an Isochrones Object + + /// The Authorization & Authentication credentials that are used for this service. + /// + /// If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. + public let credentials: Credentials + private let urlSession: URLSession + private let processingQueue: DispatchQueue + + /// The shared isochrones object. + /// + /// To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be + /// specified in the `MBXAccessToken` key in the main application bundle’s Info.plist. + public static let shared: Isochrones = .init() + + /// Creates a new instance of Isochrones object. + /// - Parameters: + /// - credentials: Credentials that will be used to make API requests to Mapbox Isochrone API. + /// - urlSession: URLSession that will be used to submit API requests to Mapbox Isochrone API. + /// - processingQueue: A DispatchQueue that will be used for CPU intensive work. + public init( + credentials: Credentials = .init(), + urlSession: URLSession = .shared, + processingQueue: DispatchQueue = .global(qos: .userInitiated) + ) { + self.credentials = credentials + self.urlSession = urlSession + self.processingQueue = processingQueue + } + + /// Begins asynchronously calculating isochrone contours using the given options and delivers the results to a + /// closure. + /// This method retrieves the contours asynchronously from the [Mapbox Isochrone + /// API](https://docs.mapbox.com/api/navigation/isochrone/) over a network connection. If a connection error or + /// server error occurs, details about the error are passed into the given completion handler in lieu of the + /// contours. + /// + /// Contours may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). + /// - Parameters: + /// - options: An ``IsochroneOptions`` object specifying the requirements for the resulting contours. + /// - completionHandler: The closure (block) to call with the resulting contours. This closure is executed on the + /// application’s main thread. + /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to + /// execute, you no longer want the resulting contours, cancel this task. + @discardableResult + open func calculate( + _ options: IsochroneOptions, + completionHandler: @escaping IsochroneCompletionHandler + ) -> URLSessionDataTask { + let request = urlRequest(forCalculating: options) + let callCompletion = { @Sendable (_ result: Result) in + _ = Task { @MainActor in + completionHandler(result) + } + } + let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in + if let urlError = possibleError as? URLError { + callCompletion(.failure(.network(urlError))) + return + } + + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { + callCompletion(.failure(.invalidResponse(possibleResponse))) + return + } + + guard let data = possibleData else { + callCompletion(.failure(.noData)) + return + } + + self.processingQueue.async { + do { + let decoder = JSONDecoder() + + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = IsochroneError( + code: nil, + message: nil, + response: possibleResponse, + underlyingError: possibleError + ) + + callCompletion(.failure(apiError)) + return + } + + guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { + let apiError = IsochroneError( + code: disposition.code, + message: disposition.message, + response: response, + underlyingError: possibleError + ) + callCompletion(.failure(apiError)) + return + } + + let result = try decoder.decode(FeatureCollection.self, from: data) + + callCompletion(.success(result)) + } catch { + let bailError = IsochroneError(code: nil, message: nil, response: response, underlyingError: error) + callCompletion(.failure(bailError)) + } + } + } + requestTask.priority = 1 + requestTask.resume() + + return requestTask + } + + // MARK: Request URL Preparation + + /// The GET HTTP URL used to fetch the contours from the API. + /// + /// - Parameter options: An ``IsochroneOptions`` object specifying the requirements for the resulting contours. + /// - Returns: The URL to send the request to. + open func url(forCalculating options: IsochroneOptions) -> URL { + var params = options.urlQueryItems + params.append(URLQueryItem(name: "access_token", value: credentials.accessToken)) + + let unparameterizedURL = URL(path: options.path, host: credentials.host) + var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! + components.queryItems = params + return components.url! + } + + /// The HTTP request used to fetch the contours from the API. + /// + /// - Parameter options: A ``IsochroneOptions`` object specifying the requirements for the resulting routes. + /// - Returns: A GET HTTP request to calculate the specified options. + open func urlRequest(forCalculating options: IsochroneOptions) -> URLRequest { + let getURL = url(forCalculating: options) + var request = URLRequest(url: getURL) + request.setupUserAgentString() + return request + } +} + +@available(*, unavailable) +extension Isochrones: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxDirections/Junction.swift b/ios/Classes/Navigation/MapboxDirections/Junction.swift new file mode 100644 index 000000000..590256382 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Junction.swift @@ -0,0 +1,14 @@ +import Foundation + +/// Contains information about routing and passing junction along the route. +public struct Junction: Codable, Equatable, Sendable { + /// The name of the junction, if available. + public let name: String? + + /// Initializes a new `Junction` object. + /// - Parameters: + /// - name: the name of the junction. + public init(name: String?) { + self.name = name + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Lane.swift b/ios/Classes/Navigation/MapboxDirections/Lane.swift new file mode 100644 index 000000000..edabce1f4 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Lane.swift @@ -0,0 +1,57 @@ +import Foundation +import Turf + +/// A lane on the road approaching an intersection. +struct Lane: Equatable, ForeignMemberContainer { + var foreignMembers: JSONObject = [:] + + /// The lane indications specifying the maneuvers that may be executed from the lane. + let indications: LaneIndication + + /// Whether the lane can be taken to complete the maneuver (`true`) or not (`false`) + var isValid: Bool + + /// Whether the lane is a preferred lane (`true`) or not (`false`) + /// + /// A preferred lane is a lane that is recommended if there are multiple lanes available + var isActive: Bool? + + /// Which of the ``indications`` is applicable to the current route, when there is more than one + var validIndication: ManeuverDirection? + + init(indications: LaneIndication, valid: Bool = false, active: Bool? = false, preferred: ManeuverDirection? = nil) { + self.indications = indications + self.isValid = valid + self.isActive = active + self.validIndication = preferred + } +} + +extension Lane: Codable { + private enum CodingKeys: String, CodingKey { + case indications + case valid + case active + case preferred = "valid_indication" + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(indications, forKey: .indications) + try container.encode(isValid, forKey: .valid) + try container.encodeIfPresent(isActive, forKey: .active) + try container.encodeIfPresent(validIndication, forKey: .preferred) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.indications = try container.decode(LaneIndication.self, forKey: .indications) + self.isValid = try container.decode(Bool.self, forKey: .valid) + self.isActive = try container.decodeIfPresent(Bool.self, forKey: .active) + self.validIndication = try container.decodeIfPresent(ManeuverDirection.self, forKey: .preferred) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/LaneIndication.swift b/ios/Classes/Navigation/MapboxDirections/LaneIndication.swift new file mode 100644 index 000000000..527e7afb5 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/LaneIndication.swift @@ -0,0 +1,134 @@ +import Foundation + +/// Each of these options specifies a maneuver direction for which a given lane can be used. +/// +/// A Lane object has zero or more indications that usually correspond to arrows on signs or pavement markings. If no +/// options are specified, it may be the case that no maneuvers are indicated on signage or pavement markings for the +/// lane. +public struct LaneIndication: OptionSet, CustomStringConvertible, Sendable { + public var rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// Indicates a sharp turn to the right. + public static let sharpRight = LaneIndication(rawValue: 1 << 1) + + /// Indicates a turn to the right. + public static let right = LaneIndication(rawValue: 1 << 2) + + /// Indicates a turn to the right. + public static let slightRight = LaneIndication(rawValue: 1 << 3) + + /// Indicates no turn. + public static let straightAhead = LaneIndication(rawValue: 1 << 4) + + /// Indicates a slight turn to the left. + public static let slightLeft = LaneIndication(rawValue: 1 << 5) + + /// Indicates a turn to the left. + public static let left = LaneIndication(rawValue: 1 << 6) + + /// Indicates a sharp turn to the left. + public static let sharpLeft = LaneIndication(rawValue: 1 << 7) + + /// Indicates a U-turn. + public static let uTurn = LaneIndication(rawValue: 1 << 8) + + /// Creates a lane indication from the given description strings. + public init?(descriptions: [String]) { + var laneIndication: LaneIndication = [] + for description in descriptions { + switch description { + case "sharp right": + laneIndication.insert(.sharpRight) + case "right": + laneIndication.insert(.right) + case "slight right": + laneIndication.insert(.slightRight) + case "straight": + laneIndication.insert(.straightAhead) + case "slight left": + laneIndication.insert(.slightLeft) + case "left": + laneIndication.insert(.left) + case "sharp left": + laneIndication.insert(.sharpLeft) + case "uturn": + laneIndication.insert(.uTurn) + case "none": + break + default: + return nil + } + } + self.init(rawValue: laneIndication.rawValue) + } + + init?(from direction: ManeuverDirection) { + // Assuming that every possible raw value of ManeuverDirection matches valid raw value of LaneIndication + self.init(descriptions: [direction.rawValue]) + } + + public var descriptions: [String] { + if isEmpty { + return [] + } + + var descriptions: [String] = [] + if contains(.sharpRight) { + descriptions.append("sharp right") + } + if contains(.right) { + descriptions.append("right") + } + if contains(.slightRight) { + descriptions.append("slight right") + } + if contains(.straightAhead) { + descriptions.append("straight") + } + if contains(.slightLeft) { + descriptions.append("slight left") + } + if contains(.left) { + descriptions.append("left") + } + if contains(.sharpLeft) { + descriptions.append("sharp left") + } + if contains(.uTurn) { + descriptions.append("uturn") + } + return descriptions + } + + public var description: String { + return descriptions.joined(separator: ",") + } + + static func indications(from strings: [String], container: SingleValueDecodingContainer) throws -> LaneIndication { + guard let indications = self.init(descriptions: strings) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Unable to initialize lane indications from decoded string. This should not happen." + ) + } + return indications + } +} + +extension LaneIndication: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let stringValues = try container.decode([String].self) + + self = try LaneIndication.indications(from: stringValues, container: container) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(descriptions) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/MapMatching/MapMatchingResponse.swift b/ios/Classes/Navigation/MapboxDirections/MapMatching/MapMatchingResponse.swift new file mode 100644 index 000000000..f21236e46 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -0,0 +1,87 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import Turf + +/// A ``MapMatchingResponse`` object is a structure that corresponds to a map matching response returned by the Mapbox +/// Map Matching API. +public struct MapMatchingResponse: ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] + + /// The raw HTTP response from the Map Matching API. + public let httpResponse: HTTPURLResponse? + + /// An array of ``Match`` objects. + public var matches: [Match]? + + /// An array of ``Match/Tracepoint`` objects that represent the location an input point was matched with, in the + /// order in which they were matched. + /// This property will be `nil` if a trace point is omitted by the Map Matching API because it is an outlier. + public var tracepoints: [Match.Tracepoint?]? + + /// The criteria for the map matching response. + public let options: MatchOptions + + /// The credentials used to make the request. + public let credentials: Credentials + + /// The time when this ``MapMatchingResponse`` object was created, which is immediately upon recieving the raw URL + /// response. + /// + /// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to + /// `nil`; use the `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be + /// set to `nil` if you create this result from a JSON object or encoded object. + /// This property does not persist after encoding and decoding. + public var created: Date = .init() +} + +extension MapMatchingResponse: Codable { + private enum CodingKeys: String, CodingKey { + case matches = "matchings" + case tracepoints + } + + public init( + httpResponse: HTTPURLResponse?, + matches: [Match]? = nil, + tracepoints: [Match.Tracepoint]? = nil, + options: MatchOptions, + credentials: Credentials + ) { + self.httpResponse = httpResponse + self.matches = matches + self.tracepoints = tracepoints + self.options = options + self.credentials = credentials + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse + + guard let options = decoder.userInfo[.options] as? MatchOptions else { + throw DirectionsCodingError.missingOptions + } + self.options = options + + guard let credentials = decoder.userInfo[.credentials] as? Credentials else { + throw DirectionsCodingError.missingCredentials + } + self.credentials = credentials + + self.tracepoints = try container.decodeIfPresent([Match.Tracepoint?].self, forKey: .tracepoints) + self.matches = try container.decodeIfPresent([Match].self, forKey: .matches) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(matches, forKey: .matches) + try container.encodeIfPresent(tracepoints, forKey: .tracepoints) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/MapMatching/Match.swift b/ios/Classes/Navigation/MapboxDirections/MapMatching/Match.swift new file mode 100644 index 000000000..ba6a69b35 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/MapMatching/Match.swift @@ -0,0 +1,163 @@ +import Foundation +import Turf + +/// A ``Weight`` enum represents the weight given to a specific ``Match`` by the Directions API. The default metric is a +/// compound index called "routability", which is duration-based with additional penalties for less desirable maneuvers. +public enum Weight: Equatable, Sendable { + case routability(value: Float) + case other(value: Float, metric: String) + + public init(value: Float, metric: String) { + switch metric { + case "routability": + self = .routability(value: value) + default: + self = .other(value: value, metric: metric) + } + } + + var metric: String { + switch self { + case .routability(value: _): + return "routability" + case .other(value: _, metric: let value): + return value + } + } + + var value: Float { + switch self { + case .routability(value: let weight): + return weight + case .other(value: let weight, metric: _): + return weight + } + } +} + +/// A ``Match`` object defines a single route that was created from a series of points that were matched against a road +/// network. +/// +/// Typically, you do not create instances of this class directly. Instead, you receive match objects when you pass a +/// ``MatchOptions`` object into the `Directions.calculate(_:completionHandler:)` method. +public struct Match: DirectionsResult { + public enum CodingKeys: String, CodingKey, CaseIterable { + case confidence + case weight + case weightName = "weight_name" + } + + public var shape: Turf.LineString? + + public var legs: [RouteLeg] + + public var distance: Turf.LocationDistance + + public var expectedTravelTime: TimeInterval + + public var typicalTravelTime: TimeInterval? + + public var speechLocale: Locale? + + public var fetchStartDate: Date? + + public var responseEndDate: Date? + + public var responseContainsSpeechLocale: Bool + + public var foreignMembers: Turf.JSONObject = [:] + + /// Initializes a match. + /// Typically, you do not create instances of this class directly. Instead, you receive match objects when you + /// request matches using the `Directions.calculate(_:completionHandler:)` method. + /// + /// - Parameters: + /// - legs: The legs that are traversed in order. + /// - shape: The matching roads or paths as a contiguous polyline. + /// - distance: The matched path’s cumulative distance, measured in meters. + /// - expectedTravelTime: The route’s expected travel time, measured in seconds. + /// - confidence: A number between 0 and 1 that indicates the Map Matching API’s confidence that the match is + /// accurate. A higher confidence means the match is more likely to be accurate. + /// - weight: A ``Weight`` enum, which represents the weight given to a specific ``Match``. + public init( + legs: [RouteLeg], + shape: LineString?, + distance: LocationDistance, + expectedTravelTime: TimeInterval, + confidence: Float, + weight: Weight + ) { + self.confidence = confidence + self.weight = weight + self.legs = legs + self.shape = shape + self.distance = distance + self.expectedTravelTime = expectedTravelTime + self.responseContainsSpeechLocale = false + } + + /// Creates a match from a decoder. + /// + /// - Precondition: If the decoder is decoding JSON data from an API response, the `Decoder.userInfo` dictionary + /// must contain a ``MatchOptions`` object in the ``Swift/CodingUserInfoKey/options`` key. If it does not, a + /// ``DirectionsCodingError/missingOptions`` error is thrown. + /// + /// - Parameter decoder: The decoder of JSON-formatted API response data or a previously encoded ``Match`` object. + public init(from decoder: Decoder) throws { + guard let options = decoder.userInfo[.options] as? DirectionsOptions else { + throw DirectionsCodingError.missingOptions + } + + let container = try decoder.container(keyedBy: DirectionsCodingKey.self) + self.legs = try Self.decodeLegs(using: container, options: options) + self.distance = try Self.decodeDistance(using: container) + self.expectedTravelTime = try Self.decodeExpectedTravelTime(using: container) + self.typicalTravelTime = try Self.decodeTypicalTravelTime(using: container) + self.shape = try Self.decodeShape(using: container) + self.speechLocale = try Self.decodeSpeechLocale(using: container) + self.responseContainsSpeechLocale = try Self.decodeResponseContainsSpeechLocale(using: container) + + self.confidence = try container.decode(Float.self, forKey: .match(.confidence)) + let weightValue = try container.decode(Float.self, forKey: .match(.weight)) + let weightMetric = try container.decode(String.self, forKey: .match(.weightName)) + + self.weight = Weight(value: weightValue, metric: weightMetric) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: DirectionsCodingKey.self) + try container.encode(confidence, forKey: .match(.confidence)) + try container.encode(weight.value, forKey: .match(.weight)) + try container.encode(weight.metric, forKey: .match(.weightName)) + + try encodeLegs(into: &container) + try encodeShape(into: &container, options: encoder.userInfo[.options] as? DirectionsOptions) + try encodeDistance(into: &container) + try encodeExpectedTravelTime(into: &container) + try encodeTypicalTravelTime(into: &container) + try encodeSpeechLocale(into: &container) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + /// A ``Weight`` enum, which represents the weight given to a specific ``Match``. + public var weight: Weight + + /// A number between 0 and 1 that indicates the Map Matching API’s confidence that the match is accurate. A higher + /// confidence means the match is more likely to be accurate. + public var confidence: Float +} + +extension Match: CustomStringConvertible { + public var description: String { + return legs.map(\.name).joined(separator: " – ") + } +} + +extension DirectionsCodingKey { + public static func match(_ key: Match.CodingKeys) -> Self { + .init(stringValue: key.stringValue) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/MapMatching/MatchOptions.swift b/ios/Classes/Navigation/MapboxDirections/MapMatching/MatchOptions.swift new file mode 100644 index 000000000..87df61750 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/MapMatching/MatchOptions.swift @@ -0,0 +1,156 @@ +import Foundation +#if canImport(CoreLocation) +import CoreLocation +#endif +import Turf + +/// A ``MatchOptions`` object is a structure that specifies the criteria for results returned by the Mapbox Map Matching +/// API. +/// +/// Pass an instance of this class into the `Directions.calculate(_:completionHandler:)` method. +open class MatchOptions: DirectionsOptions, @unchecked Sendable { + // MARK: Creating a Match Options Object + +#if canImport(CoreLocation) + /// Initializes a match options object for matching locations against the road network. + /// - Parameters: + /// - locations: An array of `CLLocation` objects representing locations to attempt to match against the road + /// network. The array should contain at least two locations (the source and destination) and at most 100 locations. + /// (Some profiles, such as ``ProfileIdentifier/automobileAvoidingTraffic``, [may have lower + /// limits](https://docs.mapbox.com/api/navigation/#directions).) + /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. + /// ``ProfileIdentifier/automobile`` is used by default. + /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. + public convenience init( + locations: [CLLocation], + profileIdentifier: ProfileIdentifier? = nil, + queryItems: [URLQueryItem]? = nil + ) { + let waypoints = locations.map { + Waypoint(location: $0) + } + self.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) + } +#endif + /// Initializes a match options object for matching geographic coordinates against the road network. + /// - Parameters: + /// - coordinates: An array of geographic coordinates representing locations to attempt to match against the road + /// network. The array should contain at least two locations (the source and destination) and at most 100 locations. + /// (Some profiles, such as ``ProfileIdentifier/automobileAvoidingTraffic``, [may have lower + /// limits](https://docs.mapbox.com/api/navigation/#directions).) Each coordinate is converted into a ``Waypoint`` + /// object. + /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. + /// ``ProfileIdentifier/automobile`` is used by default. + /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. + public convenience init( + coordinates: [LocationCoordinate2D], + profileIdentifier: ProfileIdentifier? = nil, + queryItems: [URLQueryItem]? = nil + ) { + let waypoints = coordinates.map { + Waypoint(coordinate: $0) + } + self.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) + } + + public required init( + waypoints: [Waypoint], + profileIdentifier: ProfileIdentifier? = nil, + queryItems: [URLQueryItem]? = nil + ) { + super.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) + + if queryItems?.contains(where: { queryItem in + queryItem.name == CodingKeys.resamplesTraces.stringValue && + queryItem.value == "true" + }) == true { + self.resamplesTraces = true + } + } + + private enum CodingKeys: String, CodingKey { + case resamplesTraces = "tidy" + } + + override public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(resamplesTraces, forKey: .resamplesTraces) + try super.encode(to: encoder) + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.resamplesTraces = try container.decode(Bool.self, forKey: .resamplesTraces) + try super.init(from: decoder) + } + + // MARK: Resampling the Locations Before Matching + + /// If true, the input locations are re-sampled for improved map matching results. The default is `false`. + open var resamplesTraces: Bool = false + + // MARK: Separating the Matches Into Legs + + /// An index set containing indices of two or more items in ``DirectionsOptions/waypoints``. These will be + /// represented by + /// ``Waypoint``s in the resulting ``Match`` objects. + /// + /// Use this property when the ``DirectionsOptions/includesSteps`` property is `true` or when + /// ``DirectionsOptions/waypoints`` + /// represents a trace with a high sample rate. If this property is `nil`, the resulting ``Match`` objects contain a + /// waypoint for each coordinate in the match options. + /// + /// If specified, each index must correspond to a valid index in ``DirectionsOptions/waypoints``, and the index set + /// must contain 0 + /// and the last index (one less than `endIndex`) of ``DirectionsOptions/waypoints``. + @available(*, deprecated, message: "Use Waypoint.separatesLegs instead.") + open var waypointIndices: IndexSet? + + override var legSeparators: [Waypoint] { + if let indices = (self as MatchOptionsDeprecations).waypointIndices { + return indices.map { super.waypoints[$0] } + } else { + return super.legSeparators + } + } + + // MARK: Getting the Request URL + + override open var urlQueryItems: [URLQueryItem] { + var queryItems = super.urlQueryItems + + queryItems.append(URLQueryItem(name: "tidy", value: String(describing: resamplesTraces))) + + if let waypointIndices = (self as MatchOptionsDeprecations).waypointIndices { + queryItems.append(URLQueryItem(name: "waypoints", value: waypointIndices.map { + String(describing: $0) + }.joined(separator: ";"))) + } + + return queryItems + } + + override var abridgedPath: String { + return "matching/v5/\(profileIdentifier.rawValue)" + } +} + +private protocol MatchOptionsDeprecations { + var waypointIndices: IndexSet? { get set } +} + +extension MatchOptions: MatchOptionsDeprecations {} + +@available(*, unavailable) +extension MatchOptions: @unchecked Sendable {} + +// MARK: - Equatable + +extension MatchOptions { + public static func == (lhs: MatchOptions, rhs: MatchOptions) -> Bool { + let isSuperEqual = ((lhs as DirectionsOptions) == (rhs as DirectionsOptions)) + return isSuperEqual && + lhs.abridgedPath == rhs.abridgedPath && + lhs.resamplesTraces == rhs.resamplesTraces + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/MapMatching/Tracepoint.swift b/ios/Classes/Navigation/MapboxDirections/MapMatching/Tracepoint.swift new file mode 100644 index 000000000..eb6af55a9 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/MapMatching/Tracepoint.swift @@ -0,0 +1,78 @@ +import Foundation +import Turf +#if canImport(CoreLocation) +import CoreLocation +#endif + +extension Match { + /// A tracepoint represents a location matched to the road network. + public struct Tracepoint: Codable, Equatable, Sendable { + private enum CodingKeys: String, CodingKey { + case coordinate = "location" + case countOfAlternatives = "alternatives_count" + case name + case matchingIndex = "matchings_index" + case waypointIndex = "waypoint_index" + } + + /// The geographic coordinate of the waypoint, snapped to the road network. + public var coordinate: LocationCoordinate2D + + /// Number of probable alternative matchings for this tracepoint. A value of zero indicates that this point was + /// matched unambiguously. + public var countOfAlternatives: Int + + /// The name of the road or path the coordinate snapped to. + public var name: String? + + /// The index of the match object in matchings that the sub-trace was matched to. + public var matchingIndex: Int + + /// The index of the waypoint inside the matched route. + /// + /// This value is set to`nil` for the silent waypoint when the corresponding waypoint has + /// ``Waypoint/separatesLegs`` set to `false`. + public var waypointIndex: Int? + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.coordinate = try container.decode( + LocationCoordinate2DCodable.self, + forKey: .coordinate + ).decodedCoordinates + self.countOfAlternatives = try container.decode(Int.self, forKey: .countOfAlternatives) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + self.matchingIndex = try container.decode(Int.self, forKey: .matchingIndex) + self.waypointIndex = try container.decodeIfPresent(Int.self, forKey: .waypointIndex) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(LocationCoordinate2DCodable(coordinate), forKey: .coordinate) + try container.encode(countOfAlternatives, forKey: .countOfAlternatives) + try container.encode(name, forKey: .name) + try container.encode(matchingIndex, forKey: .matchingIndex) + try container.encode(waypointIndex, forKey: .waypointIndex) + } + + public init( + coordinate: LocationCoordinate2D, + countOfAlternatives: Int, + name: String? = nil, + matchingIndex: Int = 0, + waypointIndex: Int = 0 + ) { + self.coordinate = coordinate + self.countOfAlternatives = countOfAlternatives + self.name = name + self.matchingIndex = matchingIndex + self.waypointIndex = waypointIndex + } + } +} + +extension Match.Tracepoint: CustomStringConvertible { + public var description: String { + return "" + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/MapboxDirections.h b/ios/Classes/Navigation/MapboxDirections/MapboxDirections.h new file mode 100644 index 000000000..97e6721f7 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/MapboxDirections.h @@ -0,0 +1,8 @@ +#import +#import + +//! Project version number for MapboxDirections. +FOUNDATION_EXPORT double MapboxDirectionsVersionNumber; + +//! Project version string for MapboxDirections. +FOUNDATION_EXPORT const unsigned char MapboxDirectionsVersionString[]; diff --git a/ios/Classes/Navigation/MapboxDirections/MapboxStreetsRoadClass.swift b/ios/Classes/Navigation/MapboxDirections/MapboxStreetsRoadClass.swift new file mode 100644 index 000000000..40974dfab --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/MapboxStreetsRoadClass.swift @@ -0,0 +1,56 @@ + +import Foundation + +/// A road classification according to the [Mapbox Streets +/// source](https://docs.mapbox.com/vector-tiles/reference/mapbox-streets-v8/#road) , version 8. +public enum MapboxStreetsRoadClass: String, Codable, Equatable, Sendable { + /// High-speed, grade-separated highways + case motorway + /// Link roads/lanes/ramps connecting to motorways + case motorwayLink = "motorway_link" + /// Important roads that are not motorways. + case trunk + /// Link roads/lanes/ramps connecting to trunk roads + case trunkLink = "trunk_link" + /// A major highway linking large towns. + case primary + /// Link roads/lanes connecting to primary roads + case primaryLink = "primary_link" + /// A highway linking large towns. + case secondary + /// Link roads/lanes connecting to secondary roads + case secondaryLink = "secondary_link" + /// A road linking small settlements, or the local centres of a large town or city. + case tertiary + /// Link roads/lanes connecting to tertiary roads + case tertiaryLink = "tertiary_link" + /// Standard unclassified, residential, road, and living_street road types + case street + /// Streets that may have limited or no access for motor vehicles. + case streetLimited = "street_limited" + /// Includes pedestrian streets, plazas, and public transportation platforms. + case pedestrian + /// Includes motor roads under construction (but not service roads, paths, etc. + case construction + /// Roads mostly for agricultural and forestry use etc. + case track + /// Access roads, alleys, agricultural tracks, and other services roads. Also includes parking lot aisles, public & + /// private driveways. + case service + /// Those that serves automobiles and no or unspecified automobile service. + case ferry + /// Foot paths, cycle paths, ski trails. + case path + /// Railways, including mainline, commuter rail, and rapid transit. + case majorRail = "major_rail" + /// Includes light rail & tram lines. + case minorRail = "minor_rail" + /// Yard and service railways. + case serviceRail = "service_rail" + /// Ski lifts, gondolas, and other types of aerialway. + case aerialway + /// The approximate centerline of a golf course hole + case golf + /// Undefined + case undefined +} diff --git a/ios/Classes/Navigation/MapboxDirections/Matrix.swift b/ios/Classes/Navigation/MapboxDirections/Matrix.swift new file mode 100644 index 000000000..e20b1e2f2 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Matrix.swift @@ -0,0 +1,166 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +/// Computes distances and durations between origin-destination pairs, and returns the resulting distances in meters and +/// durations in seconds. +open class Matrix: @unchecked Sendable { + /// A tuple type representing the matrix session that was generated from the request. + /// + /// - Parameter options: A ``MatrixOptions`` object representing the request parameter options. + /// - Parameter credentials: A object containing the credentials used to make the request. + public typealias Session = (options: MatrixOptions, credentials: Credentials) + + /// A closure (block) to be called when a matrix request is complete. + /// + /// - parameter result: A `Result` enum that represents the (RETURN TYPE) if the request returned successfully, or + /// the error if it did not. + public typealias MatrixCompletionHandler = @Sendable ( + _ result: Result + ) -> Void + + // MARK: Creating an Matrix Object + + /// The Authorization & Authentication credentials that are used for this service. + /// + /// If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. + public let credentials: Credentials + private let urlSession: URLSession + private let processingQueue: DispatchQueue + + /// The shared matrix object. + /// + /// To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be + /// specified in the `MBXAccessToken` key in the main application bundle’s Info.plist. + public static let shared: Matrix = .init() + + /// Creates a new instance of Matrix object. + /// - Parameters: + /// - credentials: Credentials that will be used to make API requests to Mapbox Matrix API. + /// - urlSession: URLSession that will be used to submit API requests to Mapbox Matrix API. + /// - processingQueue: A DispatchQueue that will be used for CPU intensive work. + public init( + credentials: Credentials = .init(), + urlSession: URLSession = .shared, + processingQueue: DispatchQueue = .global(qos: .userInitiated) + ) { + self.credentials = credentials + self.urlSession = urlSession + self.processingQueue = processingQueue + } + + // MARK: Getting Matrix + + @discardableResult + /// Begins asynchronously calculating matrices using the given options and delivers the results to a closure. + /// + /// This method retrieves the matrices asynchronously from the [Mapbox Matrix + /// API](https://docs.mapbox.com/api/navigation/matrix/) over a network connection. If a connection error or server + /// error occurs, details about the error are passed into the given completion handler in lieu of the contours. + /// - Parameters: + /// - options: A ``MatrixOptions`` object specifying the requirements for the resulting matrices. + /// - completionHandler: The closure (block) to call with the resulting matrices. This closure is executed on the + /// application’s main thread. + /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to + /// execute, you no longer want the resulting matrices, cancel this task. + open func calculate( + _ options: MatrixOptions, + completionHandler: @escaping MatrixCompletionHandler + ) -> URLSessionDataTask { + let request = urlRequest(forCalculating: options) + let callCompletion = { @Sendable (_ result: Result) in + completionHandler(result) + } + let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in + if let urlError = possibleError as? URLError { + callCompletion(.failure(.network(urlError))) + return + } + + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { + callCompletion(.failure(.invalidResponse(possibleResponse))) + return + } + + guard let data = possibleData else { + callCompletion(.failure(.noData)) + return + } + + self.processingQueue.async { + do { + let decoder = JSONDecoder() + + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = MatrixError( + code: nil, + message: nil, + response: response, + underlyingError: possibleError + ) + + callCompletion(.failure(apiError)) + return + } + + guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { + let apiError = MatrixError( + code: disposition.code, + message: disposition.message, + response: response, + underlyingError: possibleError + ) + + callCompletion(.failure(apiError)) + return + } + + let result = try decoder.decode(MatrixResponse.self, from: data) + + guard result.distances != nil || result.travelTimes != nil else { + callCompletion(.failure(.noRoute)) + return + } + + callCompletion(.success(result)) + + } catch { + let bailError = MatrixError(code: nil, message: nil, response: response, underlyingError: error) + callCompletion(.failure(bailError)) + } + } + } + requestTask.priority = 1 + requestTask.resume() + + return requestTask + } + + // MARK: Request URL Preparation + + /// The GET HTTP URL used to fetch the matrices from the Matrix API. + /// + /// - Parameter options: A ``MatrixOptions`` object specifying the requirements for the resulting contours. + /// - Returns: The URL to send the request to. + open func url(forCalculating options: MatrixOptions) -> URL { + var params = options.urlQueryItems + params.append(URLQueryItem(name: "access_token", value: credentials.accessToken)) + + let unparameterizedURL = URL(path: options.path, host: credentials.host) + var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! + components.queryItems = params + return components.url! + } + + /// The HTTP request used to fetch the matrices from the Matrix API. + /// + /// - Parameter options: A ``MatrixOptions`` object specifying the requirements for the resulting routes. + /// - Returns: A GET HTTP request to calculate the specified options. + open func urlRequest(forCalculating options: MatrixOptions) -> URLRequest { + let getURL = url(forCalculating: options) + var request = URLRequest(url: getURL) + request.setupUserAgentString() + return request + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/MatrixError.swift b/ios/Classes/Navigation/MapboxDirections/MatrixError.swift new file mode 100644 index 000000000..862c7934e --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/MatrixError.swift @@ -0,0 +1,63 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +/// An error that occurs when computing matrices. +public enum MatrixError: LocalizedError { + public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { + if let response = response as? HTTPURLResponse { + switch (response.statusCode, code ?? "") { + case (200, "NoRoute"): + self = .noRoute + case (404, "ProfileNotFound"): + self = .profileNotFound + case (422, "InvalidInput"): + self = .invalidInput(message: message) + case (429, _): + self = .rateLimited( + rateLimitInterval: response.rateLimitInterval, + rateLimit: response.rateLimit, + resetTime: response.rateLimitResetTime + ) + default: + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } else { + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } + + /// There is no network connection available to perform the network request. + case network(_: URLError) + + /// The server returned a response that isn’t correctly formatted. + case invalidResponse(_: URLResponse?) + + /// The server returned an empty response. + case noData + + /// The API did not find a route for the given coordinates. Check for impossible routes or incorrectly formatted + /// coordinates. + case noRoute + + /// Unrecognized profile identifier. + /// + /// Make sure the ``MatrixOptions/profileIdentifier`` option is set to one of the predefined values, such as + /// ``ProfileIdentifier/automobile``. + case profileNotFound + + /// The API recieved input that it didn't understand. + /// + /// Make sure the number of approach elements matches the number of waypoints provided, and the number of waypoints + /// does not exceed the maximum number per request. + case invalidInput(message: String?) + + /// Too many requests have been made with the same access token within a certain period of time. + /// + /// Wait before retrying. + case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) + + /// Unknown error case. Look at associated values for more details. + case unknown(response: URLResponse?, underlying: Error?, code: String?, message: String?) +} diff --git a/ios/Classes/Navigation/MapboxDirections/MatrixOptions.swift b/ios/Classes/Navigation/MapboxDirections/MatrixOptions.swift new file mode 100644 index 000000000..070b3243f --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/MatrixOptions.swift @@ -0,0 +1,204 @@ +import Foundation +import Turf + +/// Options for calculating matrices from the Mapbox Matrix service. +public class MatrixOptions: Codable { + // MARK: Creating a Matrix Options Object + + /// Initializes a matrix options object for matrices and a given profile identifier. + /// - Parameters: + /// - sources: An array of ``Waypoint`` objects representing sources. + /// - destinations: An array of ``Waypoint`` objects representing destinations. + /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. + /// + /// - Note: `sources` and `destinations` should not be empty, otherwise matrix would not make sense. Total number of + /// waypoints may differ depending on the `profileIdentifier`. [See documentation for + /// details](https://docs.mapbox.com/api/navigation/matrix/#matrix-api-restrictions-and-limits). + public init(sources: [Waypoint], destinations: [Waypoint], profileIdentifier: ProfileIdentifier) { + self.profileIdentifier = profileIdentifier + self.waypointsData = .init( + sources: sources, + destinations: destinations + ) + } + + private let waypointsData: WaypointsData + + /// A string specifying the primary mode of transportation for the contours. + public var profileIdentifier: ProfileIdentifier + + /// An array of ``Waypoint`` objects representing locations that will be in the matrix. + public var waypoints: [Waypoint] { + return waypointsData.waypoints + } + + /// Attribute options for the matrix. + /// + /// Only ``AttributeOptions/distance`` and ``AttributeOptions/expectedTravelTime`` are supported. Empty + /// `attributeOptions` will result in default + /// values assumed. + public var attributeOptions: AttributeOptions = [] + + /// The ``Waypoint`` array that should be used as destinations. + /// + /// Must not be empty. + public var destinations: [Waypoint] { + get { + waypointsData.destinations + } + set { + waypointsData.destinations = newValue + } + } + + /// The ``Waypoint`` array that should be used as sources. + /// + /// Must not be empty. + public var sources: [Waypoint] { + get { + waypointsData.sources + } + set { + waypointsData.sources = newValue + } + } + + private enum CodingKeys: String, CodingKey { + case profileIdentifier = "profile" + case attributeOptions = "annotations" + case destinations + case sources + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(profileIdentifier, forKey: .profileIdentifier) + try container.encode(attributeOptions, forKey: .attributeOptions) + try container.encodeIfPresent(destinations, forKey: .destinations) + try container.encodeIfPresent(sources, forKey: .sources) + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.profileIdentifier = try container.decode(ProfileIdentifier.self, forKey: .profileIdentifier) + self.attributeOptions = try container.decodeIfPresent(AttributeOptions.self, forKey: .attributeOptions) ?? [] + let destinations = try container.decodeIfPresent([Waypoint].self, forKey: .destinations) ?? [] + let sources = try container.decodeIfPresent([Waypoint].self, forKey: .sources) ?? [] + self.waypointsData = .init( + sources: sources, + destinations: destinations + ) + } + + // MARK: Getting the Request URL + + var coordinates: String? { + waypoints.map(\.coordinate.requestDescription).joined(separator: ";") + } + + /// An array of URL query items to include in an HTTP request. + var abridgedPath: String { + return "directions-matrix/v1/\(profileIdentifier.rawValue)" + } + + /// The path of the request URL, not including the hostname or any parameters. + var path: String { + guard let coordinates, + !coordinates.isEmpty + else { + assertionFailure("No query") + return "" + } + return "\(abridgedPath)/\(coordinates)" + } + + /// An array of URL query items (parameters) to include in an HTTP request. + public var urlQueryItems: [URLQueryItem] { + var queryItems: [URLQueryItem] = [] + + if !attributeOptions.isEmpty { + queryItems.append(URLQueryItem(name: "annotations", value: attributeOptions.description)) + } + + let mustArriveOnDrivingSide = !waypoints.filter { !$0.allowsArrivingOnOppositeSide }.isEmpty + if mustArriveOnDrivingSide { + let approaches = waypoints.map { $0.allowsArrivingOnOppositeSide ? "unrestricted" : "curb" } + queryItems.append(URLQueryItem(name: "approaches", value: approaches.joined(separator: ";"))) + } + + if waypoints.count != waypointsData.destinationsIndices.count { + let destinationString = waypointsData.destinationsIndices.map { String($0) }.joined(separator: ";") + queryItems.append(URLQueryItem(name: "destinations", value: destinationString)) + } + + if waypoints.count != waypointsData.sourcesIndices.count { + let sourceString = waypointsData.sourcesIndices.map { String($0) }.joined(separator: ";") + queryItems.append(URLQueryItem(name: "sources", value: sourceString)) + } + + return queryItems + } +} + +@available(*, unavailable) +extension MatrixOptions: @unchecked Sendable {} + +extension MatrixOptions: Equatable { + public static func == (lhs: MatrixOptions, rhs: MatrixOptions) -> Bool { + return lhs.profileIdentifier == rhs.profileIdentifier && + lhs.attributeOptions == rhs.attributeOptions && + lhs.sources == rhs.sources && + lhs.destinations == rhs.destinations + } +} + +extension MatrixOptions { + fileprivate class WaypointsData { + private(set) var waypoints: [Waypoint] = [] + var sources: [Waypoint] { + didSet { + updateWaypoints() + } + } + + var destinations: [Waypoint] { + didSet { + updateWaypoints() + } + } + + private(set) var sourcesIndices: IndexSet = [] + private(set) var destinationsIndices: IndexSet = [] + + private func updateWaypoints() { + sourcesIndices = [] + destinationsIndices = [] + + var destinations = destinations + for source in sources.enumerated() { + for destination in destinations.enumerated() { + if source.element == destination.element { + destinations.remove(at: destination.offset) + destinationsIndices.insert(source.offset) + break + } + } + } + + destinationsIndices.insert(integersIn: sources.endIndex..<(sources.endIndex + destinations.count)) + + var sum = sources + sum.append(contentsOf: destinations) + waypoints = sum + + sourcesIndices = IndexSet(integersIn: sources.indices) + } + + init(sources: [Waypoint], destinations: [Waypoint]) { + self.sources = sources + self.destinations = destinations + + updateWaypoints() + } + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/MatrixResponse.swift b/ios/Classes/Navigation/MapboxDirections/MatrixResponse.swift new file mode 100644 index 000000000..5fd2c1447 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/MatrixResponse.swift @@ -0,0 +1,128 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import Turf + +public struct MatrixResponse: Sendable { + public typealias DistanceMatrix = [[LocationDistance?]] + public typealias DurationMatrix = [[TimeInterval?]] + + public let httpResponse: HTTPURLResponse? + + public let destinations: [Waypoint]? + public let sources: [Waypoint]? + + /// Array of arrays that represent the distances matrix in row-major order. + /// + /// `distances[i][j]` gives the route distance from the `i`'th `source` to the `j`'th `destination`. The distance + /// between the same coordinate is always `0`. Distance from `i` to `j` is not always the same as from `j` to `i`. + /// If a route cannot be found, the result is `nil`. + /// + /// - SeeAlso: ``distance(from:to:)`` + public let distances: DistanceMatrix? + + /// Array of arrays that represent the travel times matrix in row-major order. + /// + /// `travelTimes[i][j]` gives the travel time from the `i`'th `source` to the `j`'th `destination`. The duration + /// between the same coordinate is always `0`. Travel time from `i` to `j` is not always the same as from `j` to + /// `i`. If a duration cannot be found, the result is `nil`. + /// + /// - SeeAlso: ``travelTime(from:to:)`` + public let travelTimes: DurationMatrix? + + /// Returns route distance between specified source and destination. + /// - Parameters: + /// - sourceIndex: Index of a waypoint in the ``sources`` array. + /// - destinationIndex: Index of a waypoint in the ``destinations`` array. + /// - Returns: Calculated route distance between the points or `nil` if it is not available. + public func distance(from sourceIndex: Int, to destinationIndex: Int) -> LocationDistance? { + guard sources?.indices.contains(sourceIndex) ?? false, + destinations?.indices.contains(destinationIndex) ?? false + else { + return nil + } + return distances?[sourceIndex][destinationIndex] + } + + /// Returns expected travel time between specified source and destination. + /// - Parameters: + /// - sourceIndex: Index of a waypoint in the ``sources`` array. + /// - destinationIndex: Index of a waypoint in the ``destinations`` array. + /// - Returns: Calculated expected travel time between the points or `nil` if it is not available. + public func travelTime(from sourceIndex: Int, to destinationIndex: Int) -> TimeInterval? { + guard sources?.indices.contains(sourceIndex) ?? false, + destinations?.indices.contains(destinationIndex) ?? false + else { + return nil + } + return travelTimes?[sourceIndex][destinationIndex] + } +} + +extension MatrixResponse: Codable { + enum CodingKeys: String, CodingKey { + case distances + case durations + case destinations + case sources + } + + public init( + httpResponse: HTTPURLResponse?, + distances: DistanceMatrix?, + travelTimes: DurationMatrix?, + destinations: [Waypoint]?, + sources: [Waypoint]? + ) { + self.httpResponse = httpResponse + self.destinations = destinations + self.sources = sources + self.distances = distances + self.travelTimes = travelTimes + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var distancesMatrix: DistanceMatrix = [] + var durationsMatrix: DurationMatrix = [] + + self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse + self.destinations = try container.decode([Waypoint].self, forKey: .destinations) + self.sources = try container.decode([Waypoint].self, forKey: .sources) + + if let decodedDistances = try container.decodeIfPresent([[Double?]].self, forKey: .distances) { + decodedDistances.forEach { distanceArray in + var distances: [LocationDistance?] = [] + distanceArray.forEach { distance in + distances.append(distance) + } + distancesMatrix.append(distances) + } + self.distances = distancesMatrix + } else { + self.distances = nil + } + + if let decodedDurations = try container.decodeIfPresent([[Double?]].self, forKey: .durations) { + decodedDurations.forEach { durationArray in + var durations: [TimeInterval?] = [] + durationArray.forEach { duration in + durations.append(duration) + } + durationsMatrix.append(durations) + } + self.travelTimes = durationsMatrix + } else { + self.travelTimes = nil + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(destinations, forKey: .destinations) + try container.encode(sources, forKey: .sources) + try container.encodeIfPresent(distances, forKey: .distances) + try container.encodeIfPresent(travelTimes, forKey: .durations) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/OfflineDirections.swift b/ios/Classes/Navigation/MapboxDirections/OfflineDirections.swift new file mode 100644 index 000000000..9379d703b --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/OfflineDirections.swift @@ -0,0 +1,134 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import Turf + +public typealias OfflineVersion = String + +public typealias OfflineDownloaderCompletionHandler = @Sendable ( + _ location: URL?, + _ response: URLResponse?, + _ error: Error? +) -> Void + +public typealias OfflineDownloaderProgressHandler = @Sendable ( + _ bytesWritten: Int64, + _ totalBytesWritten: Int64, + _ totalBytesExpectedToWrite: Int64 +) -> Void + +public typealias OfflineVersionsHandler = @Sendable ( + _ version: [OfflineVersion]?, _ error: Error? +) -> Void + +struct AvailableVersionsResponse: Codable, Sendable { + let availableVersions: [String] +} + +public protocol OfflineDirectionsProtocol { + /// Fetches the available offline routing tile versions and returns them in descending chronological order. The most + /// recent version should typically be passed into ``downloadTiles(in:version:completionHandler:)``. + /// + /// - Parameter completionHandler: A closure of type ``OfflineVersionsHandler`` which will be called when the + /// request completes + func fetchAvailableOfflineVersions( + completionHandler: @escaping OfflineVersionsHandler + ) -> URLSessionDataTask + + /// Downloads offline routing tiles of the given version within the given coordinate bounds using the shared + /// URLSession. The tiles are written to disk at the location passed into the `completionHandler`. + /// - Parameters: + /// - coordinateBounds: The bounding box. + /// - version: The version to download. Version is represented as a String (yyyy-MM-dd-x). + /// - completionHandler: A closure of type ``OfflineDownloaderCompletionHandler`` which will be called when the + /// request completes. + /// - Returns: The Url session task. + func downloadTiles( + in coordinateBounds: BoundingBox, + version: OfflineVersion, + completionHandler: @escaping OfflineDownloaderCompletionHandler + ) -> URLSessionDownloadTask +} + +extension Directions: OfflineDirectionsProtocol { + /// The URL to a list of available versions. + public var availableVersionsURL: URL { + let url = credentials.host.appendingPathComponent("route-tiles/v1").appendingPathComponent("versions") + var components = URLComponents(url: url, resolvingAgainstBaseURL: true) + components?.queryItems = [URLQueryItem(name: "access_token", value: credentials.accessToken)] + return components!.url! + } + + /// Returns the URL to generate and download a tile pack from the Route Tiles API. + /// - Parameters: + /// - coordinateBounds: The coordinate bounds that the tiles should cover. + /// - version: A version obtained from ``availableVersionsURL``. + /// - Returns: The URL to generate and download the tile pack that covers the coordinate bounds. + public func tilesURL(for coordinateBounds: BoundingBox, version: OfflineVersion) -> URL { + let url = credentials.host.appendingPathComponent("route-tiles/v1") + .appendingPathComponent(coordinateBounds.description) + var components = URLComponents(url: url, resolvingAgainstBaseURL: true) + components?.queryItems = [ + URLQueryItem(name: "version", value: version), + URLQueryItem(name: "access_token", value: credentials.accessToken), + ] + return components!.url! + } + + /// Fetches the available offline routing tile versions and returns them in descending chronological order. The most + /// recent version should typically be passed into ``downloadTiles(in:version:completionHandler:)``. + /// + /// - Parameter completionHandler: A closure of type ``OfflineVersionsHandler`` which will be called when the + /// request completes. + @discardableResult + public func fetchAvailableOfflineVersions( + completionHandler: @escaping OfflineVersionsHandler + ) -> URLSessionDataTask { + let task = URLSession.shared.dataTask(with: availableVersionsURL) { data, _, error in + if let error { + completionHandler(nil, error) + return + } + + guard let data else { + completionHandler(nil, error) + return + } + + do { + let versionResponse = try JSONDecoder().decode(AvailableVersionsResponse.self, from: data) + let availableVersions = versionResponse.availableVersions.sorted(by: >) + completionHandler(availableVersions, error) + } catch { + completionHandler(nil, error) + } + } + + task.resume() + + return task + } + + /// Downloads offline routing tiles of the given version within the given coordinate bounds using the shared + /// URLSession. The tiles are written to disk at the location passed into the `completionHandler`. + /// - Parameters: + /// - coordinateBounds: The bounding box. + /// - version: The version to download. Version is represented as a String (yyyy-MM-dd-x). + /// - completionHandler: A closure of type ``OfflineDownloaderCompletionHandler`` which will be called when the + /// request completes. + /// - Returns: The Url session task. + @discardableResult + public func downloadTiles( + in coordinateBounds: BoundingBox, + version: OfflineVersion, + completionHandler: @escaping OfflineDownloaderCompletionHandler + ) -> URLSessionDownloadTask { + let url = tilesURL(for: coordinateBounds, version: version) + let task: URLSessionDownloadTask = URLSession.shared.downloadTask(with: url) { + completionHandler($0, $1, $2) + } + task.resume() + return task + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Polyline.swift b/ios/Classes/Navigation/MapboxDirections/Polyline.swift new file mode 100644 index 000000000..bd4afe3f0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Polyline.swift @@ -0,0 +1,397 @@ +// Polyline.swift +// +// Copyright (c) 2015 Raphaël Mor +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +#if canImport(CoreLocation) +import CoreLocation +#endif + +public typealias LocationCoordinate2D = CLLocationCoordinate2D + +// MARK: - Public Classes - + +/// This class can be used for : +/// +/// - Encoding an [CLLocation] or a [CLLocationCoordinate2D] to a polyline String +/// - Decoding a polyline String to an [CLLocation] or a [CLLocationCoordinate2D] +/// - Encoding / Decoding associated levels +/// +/// it is aims to produce the same results as google's iOS sdk not as the online +/// tool which is fuzzy when it comes to rounding values +/// +/// it is based on google's algorithm that can be found here : +/// +/// :see: https://developers.google.com/maps/documentation/utilities/polylinealgorithm +public struct Polyline { + /// The array of coordinates (nil if polyline cannot be decoded) + public let coordinates: [LocationCoordinate2D]? + /// The encoded polyline + public let encodedPolyline: String + + /// The array of levels (nil if cannot be decoded, or is not provided) + public let levels: [UInt32]? + /// The encoded levels (nil if cannot be encoded, or is not provided) + public let encodedLevels: String? + +/// The array of location (computed from coordinates) +#if canImport(CoreLocation) + public var locations: [CLLocation]? { + return coordinates.map(toLocations) + } +#endif + + // MARK: - Public Methods - + + /// This designated initializer encodes a `[CLLocationCoordinate2D]` + /// + /// - parameter coordinates: The `Array` of `LocationCoordinate2D`s (that is, `CLLocationCoordinate2D`s) that you + /// want to encode + /// - parameter levels: The optional `Array` of levels that you want to encode (default: `nil`) + /// - parameter precision: The precision used for encoding (default: `1e5`) + public init(coordinates: [LocationCoordinate2D], levels: [UInt32]? = nil, precision: Double = 1e5) { + self.coordinates = coordinates + self.levels = levels + + self.encodedPolyline = encodeCoordinates(coordinates, precision: precision) + + self.encodedLevels = levels.map(encodeLevels) + } + + /// This designated initializer decodes a polyline `String` + /// + /// - parameter encodedPolyline: The polyline that you want to decode + /// - parameter encodedLevels: The levels that you want to decode (default: `nil`) + /// - parameter precision: The precision used for decoding (default: `1e5`) + public init(encodedPolyline: String, encodedLevels: String? = nil, precision: Double = 1e5) { + self.encodedPolyline = encodedPolyline + self.encodedLevels = encodedLevels + + self.coordinates = decodePolyline(encodedPolyline, precision: precision) + + self.levels = self.encodedLevels.flatMap(decodeLevels) + } + +#if canImport(CoreLocation) + /// This init encodes a `[CLLocation]` + /// + /// - parameter locations: The `Array` of `CLLocation` that you want to encode + /// - parameter levels: The optional array of levels that you want to encode (default: `nil`) + /// - parameter precision: The precision used for encoding (default: `1e5`) + public init(locations: [CLLocation], levels: [UInt32]? = nil, precision: Double = 1e5) { + let coordinates = toCoordinates(locations) + self.init(coordinates: coordinates, levels: levels, precision: precision) + } +#endif +} + +// MARK: - Public Functions - + +/// This function encodes an `[CLLocationCoordinate2D]` to a `String` +/// +/// - parameter coordinates: The `Array` of `LocationCoordinate2D`s (that is, `CLLocationCoordinate2D`s) that you want +/// to encode +/// - parameter precision: The precision used to encode coordinates (default: `1e5`) +/// +/// - returns: A `String` representing the encoded Polyline +public func encodeCoordinates(_ coordinates: [LocationCoordinate2D], precision: Double = 1e5) -> String { + var previousCoordinate = IntegerCoordinates(0, 0) + var encodedPolyline = "" + + for coordinate in coordinates { + let intLatitude = Int(round(coordinate.latitude * precision)) + let intLongitude = Int(round(coordinate.longitude * precision)) + + let coordinatesDifference = ( + intLatitude - previousCoordinate.latitude, + intLongitude - previousCoordinate.longitude + ) + + encodedPolyline += encodeCoordinate(coordinatesDifference) + + previousCoordinate = (intLatitude, intLongitude) + } + + return encodedPolyline +} + +#if canImport(CoreLocation) +/// This function encodes an `[CLLocation]` to a `String` +/// +/// - parameter coordinates: The `Array` of `CLLocation` that you want to encode +/// - parameter precision: The precision used to encode locations (default: `1e5`) +/// +/// - returns: A `String` representing the encoded Polyline +public func encodeLocations(_ locations: [CLLocation], precision: Double = 1e5) -> String { + return encodeCoordinates(toCoordinates(locations), precision: precision) +} +#endif + +/// This function encodes an `[UInt32]` to a `String` +/// +/// - parameter levels: The `Array` of `UInt32` levels that you want to encode +/// +/// - returns: A `String` representing the encoded Levels +public func encodeLevels(_ levels: [UInt32]) -> String { + return levels.reduce("") { + $0 + encodeLevel($1) + } +} + +/// This function decodes a `String` to a `[CLLocationCoordinate2D]?` +/// +/// - parameter encodedPolyline: `String` representing the encoded Polyline +/// - parameter precision: The precision used to decode coordinates (default: `1e5`) +/// +/// - returns: A `[CLLocationCoordinate2D]` representing the decoded polyline if valid, `nil` otherwise +public func decodePolyline(_ encodedPolyline: String, precision: Double = 1e5) -> [LocationCoordinate2D]? { + let data = encodedPolyline.data(using: .utf8)! + return data.withUnsafeBytes { byteArray -> [LocationCoordinate2D]? in + let length = data.count + var position = 0 + + var decodedCoordinates = [LocationCoordinate2D]() + + var lat = 0.0 + var lon = 0.0 + + while position < length { + do { + let resultingLat = try decodeSingleCoordinate( + byteArray: byteArray, + length: length, + position: &position, + precision: precision + ) + lat += resultingLat + + let resultingLon = try decodeSingleCoordinate( + byteArray: byteArray, + length: length, + position: &position, + precision: precision + ) + lon += resultingLon + } catch { + return nil + } + + decodedCoordinates.append(LocationCoordinate2D(latitude: lat, longitude: lon)) + } + + return decodedCoordinates + } +} + +#if canImport(CoreLocation) +/// This function decodes a String to a [CLLocation]? +/// +/// - parameter encodedPolyline: String representing the encoded Polyline +/// - parameter precision: The precision used to decode locations (default: 1e5) +/// +/// - returns: A [CLLocation] representing the decoded polyline if valid, nil otherwise +public func decodePolyline(_ encodedPolyline: String, precision: Double = 1e5) -> [CLLocation]? { + return decodePolyline(encodedPolyline, precision: precision).map(toLocations) +} +#endif + +/// This function decodes a `String` to an `[UInt32]` +/// +/// - parameter encodedLevels: The `String` representing the levels to decode +/// +/// - returns: A `[UInt32]` representing the decoded Levels if the `String` is valid, `nil` otherwise +public func decodeLevels(_ encodedLevels: String) -> [UInt32]? { + var remainingLevels = encodedLevels.unicodeScalars + var decodedLevels = [UInt32]() + + while remainingLevels.count > 0 { + do { + let chunk = try extractNextChunk(&remainingLevels) + let level = decodeLevel(chunk) + decodedLevels.append(level) + } catch { + return nil + } + } + + return decodedLevels +} + +// MARK: - Private - + +// MARK: Encode Coordinate + +private func encodeCoordinate(_ locationCoordinate: IntegerCoordinates) -> String { + let latitudeString = encodeSingleComponent(locationCoordinate.latitude) + let longitudeString = encodeSingleComponent(locationCoordinate.longitude) + + return latitudeString + longitudeString +} + +private func encodeSingleComponent(_ value: Int) -> String { + var intValue = value + + if intValue < 0 { + intValue = intValue << 1 + intValue = ~intValue + } else { + intValue = intValue << 1 + } + + return encodeFiveBitComponents(intValue) +} + +// MARK: Encode Levels + +private func encodeLevel(_ level: UInt32) -> String { + return encodeFiveBitComponents(Int(level)) +} + +private func encodeFiveBitComponents(_ value: Int) -> String { + var remainingComponents = value + + var fiveBitComponent = 0 + var returnString = String() + + repeat { + fiveBitComponent = remainingComponents & 0x1F + + if remainingComponents >= 0x20 { + fiveBitComponent |= 0x20 + } + + fiveBitComponent += 63 + + let char = UnicodeScalar(fiveBitComponent)! + returnString.append(String(char)) + remainingComponents = remainingComponents >> 5 + } while remainingComponents != 0 + + return returnString +} + +// MARK: Decode Coordinate + +// We use a byte array (UnsafePointer) here for performance reasons. Check with swift 2 if we can +// go back to using [Int8] +private func decodeSingleCoordinate( + byteArray: UnsafeRawBufferPointer, + length: Int, + position: inout Int, + precision: Double = 1e5 +) throws -> Double { + guard position < length else { throw PolylineError.singleCoordinateDecodingError } + + let bitMask = Int8(0x1F) + + var coordinate: Int32 = 0 + + var currentChar: Int8 + var componentCounter: Int32 = 0 + var component: Int32 = 0 + + repeat { + currentChar = Int8(byteArray[position]) - 63 + component = Int32(currentChar & bitMask) + coordinate |= (component << (5 * componentCounter)) + position += 1 + componentCounter += 1 + } while ((currentChar & 0x20) == 0x20) && (position < length) && (componentCounter < 6) + + if componentCounter == 6, (currentChar & 0x20) == 0x20 { + throw PolylineError.singleCoordinateDecodingError + } + + if (coordinate & 0x01) == 0x01 { + coordinate = ~(coordinate >> 1) + } else { + coordinate = coordinate >> 1 + } + + return Double(coordinate) / precision +} + +// MARK: Decode Levels + +private func extractNextChunk(_ encodedString: inout String.UnicodeScalarView) throws -> String { + var currentIndex = encodedString.startIndex + + while currentIndex != encodedString.endIndex { + let currentCharacterValue = Int32(encodedString[currentIndex].value) + if isSeparator(currentCharacterValue) { + let extractedScalars = encodedString[encodedString.startIndex...currentIndex] + encodedString = String + .UnicodeScalarView(encodedString[encodedString.index(after: currentIndex).. UInt32 { + let scalarArray = [] + encodedLevel.unicodeScalars + + return UInt32(agregateScalarArray(scalarArray)) +} + +private func agregateScalarArray(_ scalars: [UnicodeScalar]) -> Int32 { + let lastValue = Int32(scalars.last!.value) + + let fiveBitComponents: [Int32] = scalars.map { scalar in + let value = Int32(scalar.value) + if value != lastValue { + return (value - 63) ^ 0x20 + } else { + return value - 63 + } + } + + return Array(fiveBitComponents.reversed()).reduce(0) { ($0 << 5) | $1 } +} + +// MARK: Utilities + +enum PolylineError: Error { + case singleCoordinateDecodingError + case chunkExtractingError +} + +#if canImport(CoreLocation) +private func toCoordinates(_ locations: [CLLocation]) -> [CLLocationCoordinate2D] { + return locations.map { location in location.coordinate } +} + +private func toLocations(_ coordinates: [CLLocationCoordinate2D]) -> [CLLocation] { + return coordinates.map { coordinate in + CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) + } +} +#endif + +private func isSeparator(_ value: Int32) -> Bool { + return (value - 63) & 0x20 != 0x20 +} + +private typealias IntegerCoordinates = (latitude: Int, longitude: Int) diff --git a/ios/Classes/Navigation/MapboxDirections/ProfileIdentifier.swift b/ios/Classes/Navigation/MapboxDirections/ProfileIdentifier.swift new file mode 100644 index 000000000..82fe513c2 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/ProfileIdentifier.swift @@ -0,0 +1,48 @@ +import Foundation + +/// Options determining the primary mode of transportation. +public struct ProfileIdentifier: Codable, Hashable, RawRepresentable, Sendable { + public init(rawValue: String) { + self.rawValue = rawValue + } + + public var rawValue: String + + /// The returned directions are appropriate for driving or riding a car, truck, or motorcycle. + /// + /// This profile prioritizes fast routes by preferring high-speed roads like highways. A driving route may use a + /// ferry where necessary. + public static let automobile: ProfileIdentifier = .init(rawValue: "mapbox/driving") + + /// The returned directions are appropriate for driving or riding a car, truck, or motorcycle. + /// + /// This profile avoids traffic congestion based on current traffic data. A driving route may use a ferry where + /// necessary. + /// + /// Traffic data is available in [a number of countries and territories + /// worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/#traffic-data). Where traffic data is + /// unavailable, this profile prefers high-speed roads like highways, similar to ``ProfileIdentifier/automobile``. + /// + /// - Note: This profile is not supported by ``Isochrones`` API. + public static let automobileAvoidingTraffic: ProfileIdentifier = .init(rawValue: "mapbox/driving-traffic") + + /// The returned directions are appropriate for riding a bicycle. + /// + /// This profile prioritizes short, safe routes by avoiding highways and preferring cycling infrastructure, such as + /// bike lanes on surface streets. A cycling route may, where necessary, use other modes of transportation, such as + /// ferries or trains, or require dismounting the bicycle for a distance. + public static let cycling: ProfileIdentifier = .init(rawValue: "mapbox/cycling") + + /// The returned directions are appropriate for walking or hiking. + /// + /// This profile prioritizes short routes, making use of sidewalks and trails where available. A walking route may + /// use other modes of transportation, such as ferries or trains, where necessary. + public static let walking: ProfileIdentifier = .init(rawValue: "mapbox/walking") +} + +@available(*, deprecated, renamed: "ProfileIdentifier") +public typealias MBDirectionsProfileIdentifier = ProfileIdentifier + +/// Options determining the primary mode of transportation for the routes. +@available(*, deprecated, renamed: "ProfileIdentifier") +public typealias DirectionsProfileIdentifier = ProfileIdentifier diff --git a/ios/Classes/Navigation/MapboxDirections/QuickLook.swift b/ios/Classes/Navigation/MapboxDirections/QuickLook.swift new file mode 100644 index 000000000..5aa6863c3 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/QuickLook.swift @@ -0,0 +1,54 @@ +import Foundation +import Turf + +/// A type with a customized Quick Look representation in the Xcode debugger. +protocol CustomQuickLookConvertible { + /// Returns a [Quick Look–compatible](https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/CustomClassDisplay_in_QuickLook/CH02-std_objects_support/CH02-std_objects_support.html#//apple_ref/doc/uid/TP40014001-CH3-SW19) + /// representation for display in the Xcode debugger. + func debugQuickLookObject() -> Any? +} + +/// Returns a URL to an image representation of the given coordinates via the [Mapbox Static Images +/// API](https://docs.mapbox.com/api/maps/#static-images). +func debugQuickLookURL( + illustrating shape: LineString, + profileIdentifier: ProfileIdentifier = .automobile, + accessToken: String? = defaultAccessToken +) -> URL? { + guard let accessToken else { + return nil + } + + let styleIdentifier: String + let identifierOfLayerAboveOverlays: String + switch profileIdentifier { + case .automobileAvoidingTraffic: + styleIdentifier = "mapbox/navigation-preview-day-v4" + identifierOfLayerAboveOverlays = "waterway-label" + case .cycling, .walking: + styleIdentifier = "mapbox/outdoors-v11" + identifierOfLayerAboveOverlays = "contour-label" + default: + styleIdentifier = "mapbox/streets-v11" + identifierOfLayerAboveOverlays = "building-number-label" + } + let styleIdentifierComponent = "/\(styleIdentifier)/static" + + var allowedCharacterSet = CharacterSet.urlPathAllowed + allowedCharacterSet.remove(charactersIn: "/)") + let encodedPolyline = shape.polylineEncodedString(precision: 1e5) + .addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! + let overlaysComponent = "/path-10+3802DA-0.6(\(encodedPolyline))" + + let path = "/styles/v1\(styleIdentifierComponent)\(overlaysComponent)/auto/680x360@2x" + + var components = URLComponents() + components.queryItems = [ + URLQueryItem(name: "before_layer", value: identifierOfLayerAboveOverlays), + URLQueryItem(name: "access_token", value: accessToken), + ] + + return URL( + string: "\(defaultApiEndPointURLString ?? "https://api.mapbox.com")\(path)?\(components.percentEncodedQuery!)" + ) +} diff --git a/ios/Classes/Navigation/MapboxDirections/RefreshedRoute.swift b/ios/Classes/Navigation/MapboxDirections/RefreshedRoute.swift new file mode 100644 index 000000000..be4632bf7 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RefreshedRoute.swift @@ -0,0 +1,65 @@ +import Foundation +import Turf + +/// A skeletal route containing only the information about the route that has been refreshed. +public struct RefreshedRoute: ForeignMemberContainer, Equatable { + public var foreignMembers: JSONObject = [:] + + /// The legs along the route, starting at the first refreshed leg index. + public var legs: [RefreshedRouteLeg] +} + +extension RefreshedRoute: Codable { + enum CodingKeys: String, CodingKey { + case legs + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.legs = try container.decode([RefreshedRouteLeg].self, forKey: .legs) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(legs, forKey: .legs) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } +} + +/// A skeletal route leg containing only the information about the route leg that has been refreshed. +public struct RefreshedRouteLeg: ForeignMemberContainer, Equatable { + public var foreignMembers: JSONObject = [:] + + public var attributes: RouteLeg.Attributes + public var incidents: [Incident]? + public var closures: [RouteLeg.Closure]? +} + +extension RefreshedRouteLeg: Codable { + enum CodingKeys: String, CodingKey { + case attributes = "annotation" + case incidents + case closures + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.attributes = try container.decode(RouteLeg.Attributes.self, forKey: .attributes) + self.incidents = try container.decodeIfPresent([Incident].self, forKey: .incidents) + self.closures = try container.decodeIfPresent([RouteLeg.Closure].self, forKey: .closures) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(attributes, forKey: .attributes) + try container.encodeIfPresent(incidents, forKey: .incidents) + try container.encodeIfPresent(closures, forKey: .closures) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/ResponseDisposition.swift b/ios/Classes/Navigation/MapboxDirections/ResponseDisposition.swift new file mode 100644 index 000000000..5c46e9892 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/ResponseDisposition.swift @@ -0,0 +1,11 @@ +import Foundation + +struct ResponseDisposition: Decodable, Equatable { + var code: String? + var message: String? + var error: String? + + private enum CodingKeys: CodingKey { + case code, message, error + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/RestStop.swift b/ios/Classes/Navigation/MapboxDirections/RestStop.swift new file mode 100644 index 000000000..e43cd94b0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RestStop.swift @@ -0,0 +1,74 @@ +import Foundation +import Turf + +/// A [rest stop](https://wiki.openstreetmap.org/wiki/Tag:highway%3Drest_area) along the route. +public struct RestStop: Codable, Equatable, ForeignMemberContainer, Sendable { + public var foreignMembers: JSONObject = [:] + + /// A kind of rest stop. + public enum StopType: String, Codable, Sendable { + /// A primitive rest stop that provides parking but no additional services. + case serviceArea = "service_area" + /// A major rest stop that provides amenities such as fuel and food. + case restArea = "rest_area" + } + + /// The kind of the rest stop. + public let type: StopType + + /// The name of the rest stop, if available. + public let name: String? + + /// Facilities associated with the rest stop, if available. + public let amenities: [Amenity]? + + private enum CodingKeys: String, CodingKey { + case type + case name + case amenities + } + + /// Initializes an unnamed rest stop of a certain kind. + /// + /// - Parameter type: The kind of rest stop. + public init(type: StopType) { + self.type = type + self.name = nil + self.amenities = nil + } + + /// Initializes an optionally named rest stop of a certain kind. + /// - Parameters: + /// - type: The kind of rest stop. + /// - name: The name of the rest stop. + /// - amenities: Facilities associated with the rest stop. + public init(type: StopType, name: String?, amenities: [Amenity]?) { + self.type = type + self.name = name + self.amenities = amenities + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.type = try container.decode(StopType.self, forKey: .type) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + self.amenities = try container.decodeIfPresent([Amenity].self, forKey: .amenities) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + try container.encodeIfPresent(name, forKey: .name) + try container.encodeIfPresent(amenities, forKey: .amenities) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.type == rhs.type + && lhs.name == rhs.name + && lhs.amenities == rhs.amenities + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/RoadClassExclusionViolation.swift b/ios/Classes/Navigation/MapboxDirections/RoadClassExclusionViolation.swift new file mode 100644 index 000000000..5afef5371 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RoadClassExclusionViolation.swift @@ -0,0 +1,16 @@ + +import Foundation + +/// Exact ``RoadClasses`` exclusion violation case. +public struct RoadClassExclusionViolation: Equatable, Sendable { + /// ``RoadClasses`` that were violated at this point. + public var roadClasses: RoadClasses + /// Index of a ``Route`` inside ``RouteResponse`` where violation occured. + public var routeIndex: Int + /// Index of a ``RouteLeg`` inside ``Route`` where violation occured. + public var legIndex: Int + /// Index of a ``RouteStep`` inside ``RouteLeg`` where violation occured. + public var stepIndex: Int + /// Index of an `Intersection` inside ``RouteStep`` where violation occured. + public var intersectionIndex: Int +} diff --git a/ios/Classes/Navigation/MapboxDirections/RoadClasses.swift b/ios/Classes/Navigation/MapboxDirections/RoadClasses.swift new file mode 100644 index 000000000..cec282186 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RoadClasses.swift @@ -0,0 +1,175 @@ +import Foundation + +/// Option set that contains attributes of a road segment. +public struct RoadClasses: OptionSet, CustomStringConvertible, Sendable, Equatable { + public var rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// The road segment is [tolled](https://wiki.openstreetmap.org/wiki/Key:toll). + /// + /// This option can only be used with ``RouteOptions/roadClassesToAvoid``. + public static let toll = RoadClasses(rawValue: 1 << 1) + + /// The road segment has access restrictions. + /// + /// A road segment may have this class if there are [general access + /// restrictions](https://wiki.openstreetmap.org/wiki/Key:access) or a [high-occupancy + /// vehicle](https://wiki.openstreetmap.org/wiki/Key:hov) restriction. + /// + /// This option **cannot** be used with ``RouteOptions/roadClassesToAvoid`` or ``RouteOptions/roadClassesToAllow``. + public static let restricted = RoadClasses(rawValue: 1 << 2) + + /// The road segment is a [freeway](https://wiki.openstreetmap.org/wiki/Tag:highway%3Dmotorway) or [freeway + /// ramp](https://wiki.openstreetmap.org/wiki/Tag:highway%3Dmotorway_link). + /// + /// It may be desirable to suppress the name of the freeway when giving instructions and give instructions at fixed + /// distances before an exit (such as 1 mile or 1 kilometer ahead). + /// + /// This option can only be used with ``RouteOptions/roadClassesToAvoid``. + public static let motorway = RoadClasses(rawValue: 1 << 3) + + /// The user must travel this segment of the route by ferry. + /// + /// The user should verify that the ferry is in operation. For driving and cycling directions, the user should also + /// verify that their vehicle is permitted onboard the ferry. + /// + /// In general, the transport type of the step containing the road segment is also ``TransportType/ferry``. + /// + /// This option can only be used with ``RouteOptions/roadClassesToAvoid``. + public static let ferry = RoadClasses(rawValue: 1 << 4) + + /// The user must travel this segment of the route through a + /// [tunnel](https://wiki.openstreetmap.org/wiki/Key:tunnel). + /// + /// This option **cannot** be used with ``RouteOptions/roadClassesToAvoid`` or ``RouteOptions/roadClassesToAllow``. + public static let tunnel = RoadClasses(rawValue: 1 << 5) + + /// The road segment is a [high occupancy vehicle road](https://wiki.openstreetmap.org/wiki/Key:hov) that requires a + /// minimum of two vehicle occupants. + /// + /// This option includes high occupancy vehicle road segments that require a minimum of two vehicle occupants only, + /// not high occupancy vehicle lanes. + /// If the user is in a high-occupancy vehicle with two occupants and would accept a route that uses a [high + /// occupancy toll road](https://wikipedia.org/wiki/High-occupancy_toll_lane), specify both + /// ``RoadClasses/highOccupancyVehicle2`` and ``RoadClasses/highOccupancyToll``. Otherwise, the routes will avoid + /// any road that requires anyone to pay a toll. + /// + /// This option can only be used with ``RouteOptions/roadClassesToAllow``. + public static let highOccupancyVehicle2 = RoadClasses(rawValue: 1 << 6) + + /// The road segment is a [high occupancy vehicle road](https://wiki.openstreetmap.org/wiki/Key:hov) that requires a + /// minimum of three vehicle occupants. + /// + /// This option includes high occupancy vehicle road segments that require a minimum of three vehicle occupants + /// only, not high occupancy vehicle lanes. + /// + /// This option can only be used with ``RouteOptions/roadClassesToAllow``. + public static let highOccupancyVehicle3 = RoadClasses(rawValue: 1 << 7) + + /// The road segment is a [high occupancy toll road](https://wikipedia.org/wiki/High-occupancy_toll_lane) that is + /// tolled if the user's vehicle does not meet the minimum occupant requirement. + /// + /// This option includes high occupancy toll road segments only, not high occupancy toll lanes. + /// + /// This option can only be used with ``RouteOptions/roadClassesToAllow``. + public static let highOccupancyToll = RoadClasses(rawValue: 1 << 8) + + /// The user must travel this segment of the route on an unpaved road. + /// + /// This option can only be used with ``RouteOptions/roadClassesToAvoid``. + public static let unpaved = RoadClasses(rawValue: 1 << 9) + + /// The road segment is [tolled](https://wiki.openstreetmap.org/wiki/Key:toll) and only accepts cash payment. + /// + /// This option can only be used with ``RouteOptions/roadClassesToAvoid``. + public static let cashTollOnly = RoadClasses(rawValue: 1 << 10) + + /// Creates a ``RoadClasses`` given an array of strings. + public init?(descriptions: [String]) { + var roadClasses: RoadClasses = [] + for description in descriptions { + switch description { + case "toll": + roadClasses.insert(.toll) + case "restricted": + roadClasses.insert(.restricted) + case "motorway": + roadClasses.insert(.motorway) + case "ferry": + roadClasses.insert(.ferry) + case "tunnel": + roadClasses.insert(.tunnel) + case "hov2": + roadClasses.insert(.highOccupancyVehicle2) + case "hov3": + roadClasses.insert(.highOccupancyVehicle3) + case "hot": + roadClasses.insert(.highOccupancyToll) + case "unpaved": + roadClasses.insert(.unpaved) + case "cash_only_tolls": + roadClasses.insert(.cashTollOnly) + case "": + continue + default: + return nil + } + } + self.init(rawValue: roadClasses.rawValue) + } + + public var description: String { + var descriptions: [String] = [] + if contains(.toll) { + descriptions.append("toll") + } + if contains(.restricted) { + descriptions.append("restricted") + } + if contains(.motorway) { + descriptions.append("motorway") + } + if contains(.ferry) { + descriptions.append("ferry") + } + if contains(.tunnel) { + descriptions.append("tunnel") + } + if contains(.highOccupancyVehicle2) { + descriptions.append("hov2") + } + if contains(.highOccupancyVehicle3) { + descriptions.append("hov3") + } + if contains(.highOccupancyToll) { + descriptions.append("hot") + } + if contains(.unpaved) { + descriptions.append("unpaved") + } + if contains(.cashTollOnly) { + descriptions.append("cash_only_tolls") + } + return descriptions.joined(separator: ",") + } +} + +extension RoadClasses: Codable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description.components(separatedBy: ",").filter { !$0.isEmpty }) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let descriptions = try container.decode([String].self) + if let roadClasses = RoadClasses(descriptions: descriptions) { + self = roadClasses + } else { + throw DirectionsError.invalidResponse(nil) + } + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Route.swift b/ios/Classes/Navigation/MapboxDirections/Route.swift new file mode 100644 index 000000000..1c98e4a11 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Route.swift @@ -0,0 +1,130 @@ +import Foundation +import Turf + +/// A ``Route`` object defines a single route that the user can follow to visit a series of waypoints in order. The +/// route object includes information about the route, such as its distance and expected travel time. Depending on the +/// criteria used to calculate the route, the route object may also include detailed turn-by-turn instructions. +/// +/// Typically, you do not create instances of this class directly. Instead, you receive route objects when you request +/// directions using the `Directions.calculate(_:completionHandler:)` or +/// `Directions.calculateRoutes(matching:completionHandler:)` method. However, if you use the +/// `Directions.url(forCalculating:)` method instead, you can use `JSONDecoder` to convert the HTTP response into a +/// ``RouteResponse`` or ``MapMatchingResponse`` object and access the ``RouteResponse/routes`` or +/// ``MapMatchingResponse/matches`` property. +public struct Route: DirectionsResult { + public enum CodingKeys: String, CodingKey, CaseIterable { + case tollPrices = "toll_costs" + } + + public var shape: Turf.LineString? + + public var legs: [RouteLeg] + + public var distance: Turf.LocationDistance + + public var expectedTravelTime: TimeInterval + + public var typicalTravelTime: TimeInterval? + + public var speechLocale: Locale? + + public var fetchStartDate: Date? + + public var responseEndDate: Date? + + public var responseContainsSpeechLocale: Bool + + public var foreignMembers: Turf.JSONObject = [:] + + /// Initializes a route. + /// - Parameters: + /// - legs: The legs that are traversed in order. + /// - shape: The roads or paths taken as a contiguous polyline. + /// - distance: The route’s distance, measured in meters. + /// - expectedTravelTime: The route’s expected travel time, measured in seconds. + /// - typicalTravelTime: The route’s typical travel time, measured in seconds. + public init( + legs: [RouteLeg], + shape: LineString?, + distance: LocationDistance, + expectedTravelTime: TimeInterval, + typicalTravelTime: TimeInterval? = nil + ) { + self.legs = legs + self.shape = shape + self.distance = distance + self.expectedTravelTime = expectedTravelTime + self.typicalTravelTime = typicalTravelTime + self.responseContainsSpeechLocale = false + } + + /// Initializes a route from a decoder. + /// + /// - Precondition: If the decoder is decoding JSON data from an API response, the `Decoder.userInfo` dictionary + /// must contain a ``RouteOptions`` or ``MatchOptions`` object in the ``Swift/CodingUserInfoKey/options`` key. If it + /// does not, a ``DirectionsCodingError/missingOptions`` error is thrown. + /// - Parameter decoder: The decoder of JSON-formatted API response data or a previously encoded ``Route`` object. + public init(from decoder: Decoder) throws { + guard let options = decoder.userInfo[.options] as? DirectionsOptions else { + throw DirectionsCodingError.missingOptions + } + + let container = try decoder.container(keyedBy: DirectionsCodingKey.self) + self.tollPrices = try container.decodeIfPresent([TollPriceCoder].self, forKey: .route(.tollPrices))? + .reduce(into: []) { $0.append(contentsOf: $1.tollPrices) } + self.legs = try Self.decodeLegs(using: container, options: options) + self.distance = try Self.decodeDistance(using: container) + self.expectedTravelTime = try Self.decodeExpectedTravelTime(using: container) + self.typicalTravelTime = try Self.decodeTypicalTravelTime(using: container) + self.shape = try Self.decodeShape(using: container) + self.speechLocale = try Self.decodeSpeechLocale(using: container) + self.responseContainsSpeechLocale = try Self.decodeResponseContainsSpeechLocale(using: container) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: DirectionsCodingKey.self) + try container.encodeIfPresent(tollPrices.map { TollPriceCoder(tollPrices: $0) }, forKey: .route(.tollPrices)) + + try encodeLegs(into: &container) + try encodeShape(into: &container, options: encoder.userInfo[.options] as? DirectionsOptions) + try encodeDistance(into: &container) + try encodeExpectedTravelTime(into: &container) + try encodeTypicalTravelTime(into: &container) + try encodeSpeechLocale(into: &container) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + /// List of calculated toll costs for this route. + /// + /// This property is set to `nil` unless request ``RouteOptions/includesTollPrices`` is set to `true`. + public var tollPrices: [TollPrice]? +} + +extension Route: CustomStringConvertible { + public var description: String { + return legs.map(\.name).joined(separator: " – ") + } +} + +extension DirectionsCodingKey { + static func route(_ key: Route.CodingKeys) -> Self { + .init(stringValue: key.rawValue) + } +} + +extension Route: Equatable { + public static func == (lhs: Route, rhs: Route) -> Bool { + return lhs.distance == rhs.distance && + lhs.expectedTravelTime == rhs.expectedTravelTime && + lhs.typicalTravelTime == rhs.typicalTravelTime && + lhs.speechLocale == rhs.speechLocale && + lhs.responseContainsSpeechLocale == rhs.responseContainsSpeechLocale && + lhs.legs == rhs.legs && + lhs.shape == rhs.shape && + lhs.tollPrices.map { Set($0) } == rhs.tollPrices + .map { Set($0) } // comparing sets to mitigate items reordering caused by custom Coding impl. + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteLeg.swift b/ios/Classes/Navigation/MapboxDirections/RouteLeg.swift new file mode 100644 index 000000000..ef1e88499 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RouteLeg.swift @@ -0,0 +1,549 @@ +import Foundation +import Turf + +/// A ``RouteLeg`` object defines a single leg of a route between two waypoints. If the overall route has only two +/// waypoints, it has a single ``RouteLeg`` object that covers the entire route. The route leg object includes +/// information about the leg, such as its name, distance, and expected travel time. Depending on the criteria used to +/// calculate the route, the route leg object may also include detailed turn-by-turn instructions. +/// +/// You do not create instances of this class directly. Instead, you receive route leg objects as part of route objects +/// when you request directions using the `Directions.calculate(_:completionHandler:)` method. +public struct RouteLeg: Codable, ForeignMemberContainer, Equatable, Sendable { + public var foreignMembers: JSONObject = [:] + + /// Foreign attribute arrays associated with this leg. + /// + /// This library does not officially support any attribute that is documented as a “beta” annotation type in the + /// Mapbox Directions API response format, but you can get and set it as an element of this `JSONObject`. It is + /// round-tripped to the `annotation` property in JSON. + /// + /// For non-attribute-related foreign members, use the ``foreignMembers`` property. + public var attributesForeignMembers: JSONObject = [:] + + public enum CodingKeys: String, CodingKey, CaseIterable { + case source + case destination + case steps + case name = "summary" + case distance + case expectedTravelTime = "duration" + case typicalTravelTime = "duration_typical" + case profileIdentifier + case annotation + case administrativeRegions = "admins" + case incidents + case viaWaypoints = "via_waypoints" + case closures + } + + // MARK: Creating a Leg + + /// Initializes a route leg. + /// - Parameters: + /// - steps: The steps that are traversed in order. + /// - name: A name that describes the route leg. + /// - distance: The route leg’s expected travel time, measured in seconds. + /// - expectedTravelTime: The route leg’s expected travel time, measured in seconds. + /// - typicalTravelTime: The route leg’s typical travel time, measured in seconds. + /// - profileIdentifier: The primary mode of transportation for the route leg. + public init( + steps: [RouteStep], + name: String, + distance: Turf.LocationDistance, + expectedTravelTime: TimeInterval, + typicalTravelTime: TimeInterval? = nil, + profileIdentifier: ProfileIdentifier + ) { + self.steps = steps + self.name = name + self.distance = distance + self.expectedTravelTime = expectedTravelTime + self.typicalTravelTime = typicalTravelTime + self.profileIdentifier = profileIdentifier + + self.segmentDistances = nil + self.expectedSegmentTravelTimes = nil + self.segmentSpeeds = nil + self.segmentCongestionLevels = nil + self.segmentNumericCongestionLevels = nil + } + + /// Creates a route leg from a decoder. + /// - Precondition: If the decoder is decoding JSON data from an API response, the `Decoder.userInfo` dictionary + /// must contain a ``RouteOptions`` or ``MatchOptions`` object in the ``Swift/CodingUserInfoKey/options`` key. If it + /// does not, a ``DirectionsCodingError/missingOptions`` error is thrown. + /// - parameter decoder: The decoder of JSON-formatted API response data or a previously encoded ``RouteLeg`` + /// object. + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.source = try container.decodeIfPresent(Waypoint.self, forKey: .source) + self.destination = try container.decodeIfPresent(Waypoint.self, forKey: .destination) + self.name = try container.decode(String.self, forKey: .name) + self.distance = try container.decode(Turf.LocationDistance.self, forKey: .distance) + self.expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime) + self.typicalTravelTime = try container.decodeIfPresent(TimeInterval.self, forKey: .typicalTravelTime) + + if let profileIdentifier = try container.decodeIfPresent(ProfileIdentifier.self, forKey: .profileIdentifier) { + self.profileIdentifier = profileIdentifier + } else if let options = decoder.userInfo[.options] as? DirectionsOptions { + self.profileIdentifier = options.profileIdentifier + } else { + throw DirectionsCodingError.missingOptions + } + + if let admins = try container.decodeIfPresent([AdministrativeRegion].self, forKey: .administrativeRegions) { + self.administrativeRegions = admins + self.steps = try RouteStep.decode( + from: container.superDecoder(forKey: .steps), + administrativeRegions: administrativeRegions! + ) + } else { + self.steps = try container.decode([RouteStep].self, forKey: .steps) + } + + if let attributes = try container.decodeIfPresent(Attributes.self, forKey: .annotation) { + self.attributes = attributes + self.attributesForeignMembers = attributes.foreignMembers + } + + if let incidents = try container.decodeIfPresent([Incident].self, forKey: .incidents) { + self.incidents = incidents + } + + if let closures = try container.decodeIfPresent([Closure].self, forKey: .closures) { + self.closures = closures + } + + if let viaWaypoints = try container.decodeIfPresent([SilentWaypoint].self, forKey: .viaWaypoints) { + self.viaWaypoints = viaWaypoints + } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(source, forKey: .source) + try container.encode(destination, forKey: .destination) + try container.encode(steps, forKey: .steps) + try container.encode(name, forKey: .name) + try container.encode(distance, forKey: .distance) + try container.encode(expectedTravelTime, forKey: .expectedTravelTime) + try container.encodeIfPresent(typicalTravelTime, forKey: .typicalTravelTime) + try container.encode(profileIdentifier, forKey: .profileIdentifier) + + var attributes = attributes + if !attributes.isEmpty { + attributes.foreignMembers = attributesForeignMembers + try container.encode(attributes, forKey: .annotation) + } + + if let admins = administrativeRegions { + try container.encode(admins, forKey: .administrativeRegions) + } + + if let incidents { + try container.encode(incidents, forKey: .incidents) + } + if let closures { + try container.encode(closures, forKey: .closures) + } + + if let viaWaypoints { + try container.encode(viaWaypoints, forKey: .viaWaypoints) + } + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + // MARK: Getting the Endpoints of the Leg + + /// The starting point of the route leg. + /// + /// Unless this is the first leg of the route, the source of this leg is the same as the destination of the previous + /// leg. + /// + /// This property is set to `nil` if the leg was decoded from a JSON RouteLeg object. + public var source: Waypoint? + + /// The endpoint of the route leg. + /// + /// Unless this is the last leg of the route, the destination of this leg is the same as the source of the next leg. + /// + /// This property is set to `nil` if the leg was decoded from a JSON RouteLeg object. + public var destination: Waypoint? + + // MARK: Getting the Steps Along the Leg + + /// An array of one or more ``RouteStep`` objects representing the steps for traversing this leg of the route. + /// + /// Each route step object corresponds to a distinct maneuver and the approach to the next maneuver. + /// + /// This array is empty if the original ``RouteOptions`` object’s ``DirectionsOptions/includesSteps`` property is + /// set to + /// `false`. + public let steps: [RouteStep] + + /// The ranges of each step’s segments within the overall leg. + /// + /// Each range corresponds to an element of the ``steps`` property. Use this property to safely subscript + /// segment-based properties such as ``segmentCongestionLevels`` and ``segmentMaximumSpeedLimits``. + /// + /// This array is empty if the original ``RouteOptions`` object’s ``DirectionsOptions/includesSteps`` property is + /// set to + /// `false`. + public private(set) lazy var segmentRangesByStep: [Range] = { + var segmentRangesByStep: [Range] = [] + var currentStepStartIndex = 0 + for step in steps { + if let coordinates = step.shape?.coordinates { + let stepCoordinateCount = step.maneuverType == .arrive ? 0 : coordinates.dropLast().count + let currentStepEndIndex = currentStepStartIndex.advanced(by: stepCoordinateCount) + segmentRangesByStep.append(currentStepStartIndex..?]? + + /// An array of ``RouteLeg/Closure`` objects describing live-traffic related closures that occur along the route. + /// + /// This information is only available for the `mapbox/driving-traffic` profile and when + /// ``DirectionsOptions/attributeOptions`` property contains ``AttributeOptions/closures``. + public var closures: [Closure]? + + /// The tendency value conveys the changing state of traffic congestion (increasing, decreasing, constant etc). + public var trafficTendencies: [TrafficTendency]? + + /// The full collection of attributes along the leg. + var attributes: Attributes { + get { + return Attributes( + segmentDistances: segmentDistances, + expectedSegmentTravelTimes: expectedSegmentTravelTimes, + segmentSpeeds: segmentSpeeds, + segmentCongestionLevels: segmentCongestionLevels, + segmentNumericCongestionLevels: segmentNumericCongestionLevels, + segmentMaximumSpeedLimits: segmentMaximumSpeedLimits, + trafficTendencies: trafficTendencies + ) + } + set { + segmentDistances = newValue.segmentDistances + expectedSegmentTravelTimes = newValue.expectedSegmentTravelTimes + segmentSpeeds = newValue.segmentSpeeds + segmentCongestionLevels = newValue.segmentCongestionLevels + segmentNumericCongestionLevels = newValue.segmentNumericCongestionLevels + segmentMaximumSpeedLimits = newValue.segmentMaximumSpeedLimits + trafficTendencies = newValue.trafficTendencies + } + } + + mutating func refreshAttributes(newAttributes: Attributes, startLegShapeIndex: Int = 0) { + let refreshRange = PartialRangeFrom(startLegShapeIndex) + + segmentDistances?.replaceIfPossible(subrange: refreshRange, with: newAttributes.segmentDistances) + expectedSegmentTravelTimes?.replaceIfPossible( + subrange: refreshRange, + with: newAttributes.expectedSegmentTravelTimes + ) + segmentSpeeds?.replaceIfPossible(subrange: refreshRange, with: newAttributes.segmentSpeeds) + segmentCongestionLevels?.replaceIfPossible(subrange: refreshRange, with: newAttributes.segmentCongestionLevels) + segmentNumericCongestionLevels?.replaceIfPossible( + subrange: refreshRange, + with: newAttributes.segmentNumericCongestionLevels + ) + segmentMaximumSpeedLimits?.replaceIfPossible( + subrange: refreshRange, + with: newAttributes.segmentMaximumSpeedLimits + ) + trafficTendencies?.replaceIfPossible(subrange: refreshRange, with: newAttributes.trafficTendencies) + } + + private func adjustShapeIndexRange(_ range: Range, startLegShapeIndex: Int) -> Range { + let startIndex = startLegShapeIndex + range.lowerBound + let endIndex = startLegShapeIndex + range.upperBound + return startIndex.. String? { + // check index ranges + guard let administrativeRegions, + stepIndex < steps.count, + intersectionIndex < steps[stepIndex].administrativeAreaContainerByIntersection?.count ?? -1, + let adminIndex = steps[stepIndex].administrativeAreaContainerByIntersection?[intersectionIndex] + else { + return nil + } + return administrativeRegions[adminIndex].countryCode + } + + // MARK: Getting Statistics About the Leg + + /// A name that describes the route leg. + /// + /// The name describes the leg using the most significant roads or trails along the route leg. You can display this + /// string to the user to help the user can distinguish one route from another based on how the legs of the routes + /// are named. + /// + /// The leg’s name does not identify the start and end points of the leg. To distinguish one leg from another within + /// the same route, concatenate the ``name`` properties of the ``source`` and ``destination`` waypoints. + public let name: String + + /// The route leg’s distance, measured in meters. + /// + /// The value of this property accounts for the distance that the user must travel to arrive at the destination from + /// the source. It is not the direct distance between the source and destination, nor should not assume that the + /// user would travel along this distance at a fixed speed. + public let distance: Turf.LocationDistance + + /// The route leg’s expected travel time, measured in seconds. + /// + /// The value of this property reflects the time it takes to traverse the route leg. If the route was calculated + /// using the ``ProfileIdentifier/automobileAvoidingTraffic`` profile, this property reflects current traffic + /// conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin + /// this leg. For other profiles, this property reflects travel time under ideal conditions and does not account for + /// traffic congestion. If the leg makes use of a ferry or train, the actual travel time may additionally be subject + /// to the schedules of those services. + /// + /// Do not assume that the user would travel along the leg at a fixed speed. For the expected travel time on each + /// individual segment along the leg, use the ``RouteStep/expectedTravelTime`` property. For more granularity, + /// specify the ``AttributeOptions/expectedTravelTime`` option and use the ``expectedSegmentTravelTimes`` property. + public var expectedTravelTime: TimeInterval + + /// The administrative regions through which the leg passes. + /// + /// Items are ordered by appearance, most recent one is at the beginning. This property is set to `nil` if no + /// administrative region data is available. + /// You can alse refer to ``Intersection/regionCode`` to get corresponding region string code. + public var administrativeRegions: [AdministrativeRegion]? + + /// Contains ``Incident``s data which occur during current ``RouteLeg``. + /// + /// Items are ordered by appearance, most recent one is at the beginning. + /// This property is set to `nil` if incidents data is not available. + public var incidents: [Incident]? + + /// Describes where a particular ``Waypoint`` passed to ``RouteOptions`` matches to the route along a ``RouteLeg``. + /// + /// The property is non-nil when for one or several ``Waypoint`` objects passed to ``RouteOptions`` have + /// ``Waypoint/separatesLegs`` property set to `false`. + public var viaWaypoints: [SilentWaypoint]? + + /// The route leg’s typical travel time, measured in seconds. + /// + /// The value of this property reflects the typical time it takes to traverse the route leg. This property is + /// available when using the ``ProfileIdentifier/automobileAvoidingTraffic`` profile. This property reflects typical + /// traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the + /// user would begin this leg. If the leg makes use of a ferry, the typical travel time may additionally be subject + /// to the schedule of this service. + /// + /// Do not assume that the user would travel along the route at a fixed speed. For more granular typical travel + /// times, use the ``RouteStep/typicalTravelTime`` property. + public var typicalTravelTime: TimeInterval? + + // MARK: Reproducing the Route + + /// The primary mode of transportation for the route leg. + /// + /// The value of this property depends on the ``DirectionsOptions/profileIdentifier`` property of the original + /// ``RouteOptions`` object. This property reflects the primary mode of transportation used for the route leg. + /// Individual steps along the route leg might use different modes of transportation as necessary. + public let profileIdentifier: ProfileIdentifier +} + +extension RouteLeg: CustomStringConvertible { + public var description: String { + return name + } +} + +extension RouteLeg: CustomQuickLookConvertible { + func debugQuickLookObject() -> Any? { + let coordinates = steps.reduce([]) { $0 + ($1.shape?.coordinates ?? []) } + guard !coordinates.isEmpty else { + return nil + } + return debugQuickLookURL(illustrating: LineString(coordinates)) + } +} + +extension RouteLeg { + /// Live-traffic related closure that occured along the route. + public struct Closure: Codable, Equatable, ForeignMemberContainer, Sendable { + public var foreignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey { + case geometryIndexStart = "geometry_index_start" + case geometryIndexEnd = "geometry_index_end" + } + + /// The range of segments within the current leg, where the closure spans. + public var shapeIndexRange: Range + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let geometryIndexStart = try container.decode(Int.self, forKey: .geometryIndexStart) + let geometryIndexEnd = try container.decode(Int.self, forKey: .geometryIndexEnd) + self.shapeIndexRange = geometryIndexStart.., with newElements: Array?) { + guard let newElements, !newElements.isEmpty else { return } + let upperBound = subrange.lowerBound + newElements.count + + guard count >= upperBound else { return } + + let adjustedSubrange = subrange.lowerBound.. Bool { + return lhs.source == rhs.source && + lhs.destination == rhs.destination && + lhs.steps == rhs.steps && + lhs.segmentDistances == rhs.segmentDistances && + lhs.expectedSegmentTravelTimes == rhs.expectedSegmentTravelTimes && + lhs.segmentSpeeds == rhs.segmentSpeeds && + lhs.segmentCongestionLevels == rhs.segmentCongestionLevels && + lhs.segmentNumericCongestionLevels == rhs.segmentNumericCongestionLevels && + lhs.segmentMaximumSpeedLimits == rhs.segmentMaximumSpeedLimits && + lhs.trafficTendencies == rhs.trafficTendencies && + lhs.name == rhs.name && + lhs.distance == rhs.distance && + lhs.expectedTravelTime == rhs.expectedTravelTime && + lhs.administrativeRegions == rhs.administrativeRegions && + lhs.incidents == rhs.incidents && + lhs.viaWaypoints == rhs.viaWaypoints && + lhs.typicalTravelTime == rhs.typicalTravelTime && + lhs.profileIdentifier == rhs.profileIdentifier + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteLegAttributes.swift b/ios/Classes/Navigation/MapboxDirections/RouteLegAttributes.swift new file mode 100644 index 000000000..55733321c --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RouteLegAttributes.swift @@ -0,0 +1,147 @@ +import Foundation +import Turf + +extension RouteLeg { + /// A collection of per-segment attributes along a route leg. + public struct Attributes: Equatable, ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] + + /// An array containing the distance (measured in meters) between each coordinate in the route leg geometry. + /// + /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains + /// ``AttributeOptions/distance``. + public var segmentDistances: [LocationDistance]? + + /// An array containing the expected travel time (measured in seconds) between each coordinate in the route leg + /// geometry. + /// + /// These values are dynamic, accounting for any conditions that may change along a segment, such as traffic + /// congestion if the profile identifier is set to ``ProfileIdentifier/automobileAvoidingTraffic`. + /// + /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains + /// `AttributeOptions.expectedTravelTime`. + public var expectedSegmentTravelTimes: [TimeInterval]? + + /// An array containing the expected average speed (measured in meters per second) between each coordinate in + /// the route leg geometry. + /// + /// These values are dynamic; rather than speed limits, they account for the road’s classification and/or any + /// traffic congestion (if the profile identifier is set to ``ProfileIdentifier/automobileAvoidingTraffic`). + /// + /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains + /// ``AttributeOptions/speed``. + public var segmentSpeeds: [LocationSpeed]? + + /// An array containing the traffic congestion level along each road segment in the route leg geometry. + /// + /// Traffic data is available in [a number of countries and territories + /// worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/#traffic-data). + /// + /// You can color-code a route line according to the congestion level along each segment of the route. + /// + /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains + /// ``AttributeOptions/congestionLevel``. + public var segmentCongestionLevels: [CongestionLevel]? + + /// An array containing the traffic congestion level along each road segment in the route leg geometry. + /// + /// Traffic data is available in [a number of countries and territories + /// worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/#traffic-data). + /// + /// You can color-code a route line according to the congestion level along each segment of the route. + /// + /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains + /// ``AttributeOptions/numericCongestionLevel``. + public var segmentNumericCongestionLevels: [NumericCongestionLevel?]? + + /// An array containing the maximum speed limit along each road segment along the route leg’s shape. + /// + /// The maximum speed may be an advisory speed limit for segments where legal limits are not posted, such as + /// highway entrance and exit ramps. If the speed limit along a particular segment is unknown, it is represented + /// in the array by a measurement whose value is negative. If the speed is unregulated along the segment, such + /// as on the German _Autobahn_ system, it is represented by a measurement whose value is `Double.infinity`. + /// + /// Speed limit data is available in [a number of countries and territories + /// worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/). + /// + /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains + /// ``AttributeOptions/maximumSpeedLimit``. + public var segmentMaximumSpeedLimits: [Measurement?]? + + /// The tendency value conveys the changing state of traffic congestion (increasing, decreasing, constant etc). + public var trafficTendencies: [TrafficTendency]? + } +} + +extension RouteLeg.Attributes: Codable { + private enum CodingKeys: String, CodingKey { + case segmentDistances = "distance" + case expectedSegmentTravelTimes = "duration" + case segmentSpeeds = "speed" + case segmentCongestionLevels = "congestion" + case segmentNumericCongestionLevels = "congestion_numeric" + case segmentMaximumSpeedLimits = "maxspeed" + case trafficTendencies = "traffic_tendency" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + segmentDistances = try container.decodeIfPresent([LocationDistance].self, forKey: .segmentDistances) + expectedSegmentTravelTimes = try container.decodeIfPresent( + [TimeInterval].self, + forKey: .expectedSegmentTravelTimes + ) + segmentSpeeds = try container.decodeIfPresent([LocationSpeed].self, forKey: .segmentSpeeds) + segmentCongestionLevels = try container.decodeIfPresent( + [CongestionLevel].self, + forKey: .segmentCongestionLevels + ) + segmentNumericCongestionLevels = try container.decodeIfPresent( + [NumericCongestionLevel?].self, + forKey: .segmentNumericCongestionLevels + ) + + if let speedLimitDescriptors = try container.decodeIfPresent( + [SpeedLimitDescriptor].self, + forKey: .segmentMaximumSpeedLimits + ) { + segmentMaximumSpeedLimits = speedLimitDescriptors.map { Measurement(speedLimitDescriptor: $0) } + } else { + segmentMaximumSpeedLimits = nil + } + + trafficTendencies = try container.decodeIfPresent([TrafficTendency].self, forKey: .trafficTendencies) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(segmentDistances, forKey: .segmentDistances) + try container.encodeIfPresent(expectedSegmentTravelTimes, forKey: .expectedSegmentTravelTimes) + try container.encodeIfPresent(segmentSpeeds, forKey: .segmentSpeeds) + try container.encodeIfPresent(segmentCongestionLevels, forKey: .segmentCongestionLevels) + try container.encodeIfPresent(segmentNumericCongestionLevels, forKey: .segmentNumericCongestionLevels) + + if let speedLimitDescriptors = segmentMaximumSpeedLimits?.map({ SpeedLimitDescriptor(speed: $0) }) { + try container.encode(speedLimitDescriptors, forKey: .segmentMaximumSpeedLimits) + } + + try container.encodeIfPresent(trafficTendencies, forKey: .trafficTendencies) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + /// Returns whether any attributes are non-nil. + var isEmpty: Bool { + return segmentDistances == nil && + expectedSegmentTravelTimes == nil && + segmentSpeeds == nil && + segmentCongestionLevels == nil && + segmentNumericCongestionLevels == nil && + segmentMaximumSpeedLimits == nil && + trafficTendencies == nil + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteOptions.swift b/ios/Classes/Navigation/MapboxDirections/RouteOptions.swift new file mode 100644 index 000000000..c6f952358 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RouteOptions.swift @@ -0,0 +1,657 @@ +import Foundation +#if canImport(CoreLocation) +import CoreLocation +#endif +import Turf + +/// A ``RouteOptions`` object is a structure that specifies the criteria for results returned by the Mapbox Directions +/// API. +/// +/// Pass an instance of this class into the `Directions.calculate(_:completionHandler:)` method. +open class RouteOptions: DirectionsOptions, @unchecked Sendable { + // MARK: Creating a Route Options Object + + /// Initializes a route options object for routes between the given waypoints and an optional profile identifier. + /// - Parameters: + /// - waypoints: An array of ``Waypoint`` objects representing locations that the route should visit in + /// chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 + /// waypoints. (Some profiles, such as ``ProfileIdentifier/automobileAvoidingTraffic``, [may have lower + /// limits](https://www.mapbox.com/api-documentation/#directions).) + /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. + /// ``ProfileIdentifier/automobile`` is used by default. + /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. + public required init( + waypoints: [Waypoint], + profileIdentifier: ProfileIdentifier? = nil, + queryItems: [URLQueryItem]? = nil + ) { + let profilesDisallowingUTurns: [ProfileIdentifier] = [.automobile, .automobileAvoidingTraffic] + self.allowsUTurnAtWaypoint = !profilesDisallowingUTurns.contains(profileIdentifier ?? .automobile) + super.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) + + guard let queryItems else { + return + } + + let mappedQueryItems = [String: String]( + queryItems.compactMap { + guard let value = $0.value else { return nil } + return ($0.name, value) + }, + uniquingKeysWith: { _, latestValue in + return latestValue + } + ) + + if mappedQueryItems[CodingKeys.includesAlternativeRoutes.stringValue] == "true" { + self.includesAlternativeRoutes = true + } + if mappedQueryItems[CodingKeys.includesExitRoundaboutManeuver.stringValue] == "true" { + self.includesExitRoundaboutManeuver = true + } + if let mappedValue = mappedQueryItems[CodingKeys.alleyPriority.stringValue], + let alleyPriority = Double(mappedValue) + { + self.alleyPriority = DirectionsPriority(rawValue: alleyPriority) + } + if let mappedValue = mappedQueryItems[CodingKeys.walkwayPriority.stringValue], + let walkwayPriority = Double(mappedValue) + { + self.walkwayPriority = DirectionsPriority(rawValue: walkwayPriority) + } + if let mappedValue = mappedQueryItems[CodingKeys.speed.stringValue], + let speed = LocationSpeed(mappedValue) + { + self.speed = speed + } + if let mappedValue = mappedQueryItems[CodingKeys.roadClassesToAvoid.stringValue], + let roadClassesToAvoid = RoadClasses(descriptions: mappedValue.components(separatedBy: ",")) + { + self.roadClassesToAvoid = roadClassesToAvoid + } + if let mappedValue = mappedQueryItems[CodingKeys.roadClassesToAllow.stringValue], + let roadClassesToAllow = RoadClasses(descriptions: mappedValue.components(separatedBy: ",")) + { + self.roadClassesToAllow = roadClassesToAllow + } + if mappedQueryItems[CodingKeys.refreshingEnabled.stringValue] == "true", profileIdentifier == + .automobileAvoidingTraffic + { + self.refreshingEnabled = true + } + + // Making copy of waypoints processed by super class to further update them... + var waypoints = self.waypoints + if let mappedValue = mappedQueryItems[CodingKeys.waypointTargets.stringValue] { + var waypointsIndex = waypoints.startIndex + let mappedValues = mappedValue.components(separatedBy: ";") + var mappedValuesIndex = mappedValues.startIndex + + while waypointsIndex < waypoints.endIndex, + mappedValuesIndex < mappedValues.endIndex + { + guard waypoints[waypointsIndex].separatesLegs else { + waypointsIndex = waypoints.index(after: waypointsIndex); continue + } + + let coordinatesComponents = mappedValues[mappedValuesIndex].components(separatedBy: ",") + waypoints[waypointsIndex].targetCoordinate = LocationCoordinate2D( + latitude: LocationDegrees(coordinatesComponents.last!)!, + longitude: LocationDegrees(coordinatesComponents.first!)! + ) + waypointsIndex = waypoints.index(after: waypointsIndex) + mappedValuesIndex = mappedValues.index(after: mappedValuesIndex) + } + } + if let mappedValue = mappedQueryItems[CodingKeys.initialManeuverAvoidanceRadius.stringValue], + let initialManeuverAvoidanceRadius = LocationDistance(mappedValue) + { + self.initialManeuverAvoidanceRadius = initialManeuverAvoidanceRadius + } + if let mappedValue = mappedQueryItems[CodingKeys.maximumHeight.stringValue], + let doubleValue = Double(mappedValue) + { + self.maximumHeight = Measurement(value: doubleValue, unit: UnitLength.meters) + } + if let mappedValue = mappedQueryItems[CodingKeys.maximumWidth.stringValue], + let doubleValue = Double(mappedValue) + { + self.maximumWidth = Measurement(value: doubleValue, unit: UnitLength.meters) + } + if let mappedValue = mappedQueryItems[CodingKeys.maximumWeight.stringValue], + let doubleValue = Double(mappedValue) + { + self.maximumWeight = Measurement(value: doubleValue, unit: UnitMass.metricTons) + } + + if let mappedValue = mappedQueryItems[CodingKeys.layers.stringValue] { + let mappedValues = mappedValue.components(separatedBy: ";") + var waypointsIndex = waypoints.startIndex + for mappedValue in mappedValues { + guard waypointsIndex < waypoints.endIndex else { break } + waypoints[waypointsIndex].layer = Int(mappedValue) ?? nil + waypointsIndex = waypoints.index(after: waypointsIndex) + } + } + + let formatter = DateFormatter.ISO8601DirectionsFormatter() + if let mappedValue = mappedQueryItems[CodingKeys.departAt.stringValue], + let departAt = formatter.date(from: mappedValue) + { + self.departAt = departAt + } + if let mappedValue = mappedQueryItems[CodingKeys.arriveBy.stringValue], + let arriveBy = formatter.date(from: mappedValue) + { + self.arriveBy = arriveBy + } + if mappedQueryItems[CodingKeys.includesTollPrices.stringValue] == "true" { + self.includesTollPrices = true + } + self.waypoints = waypoints + } + +#if canImport(CoreLocation) + /// Initializes a route options object for routes between the given locations and an optional profile identifier. + /// + /// - Note: This initializer is intended for `CLLocation` objects created using the + /// `CLLocation.init(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a + /// `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid + /// overfitting, since the ``Waypoint`` class’s `coordinateAccuracy` property represents the maximum allowed + /// deviation from the waypoint. + /// - Parameters: + /// - locations: An array of `CLLocation` objects representing locations that the route should visit in + /// chronological order. The array should contain at least two locations (the source and destination) and at most 25 + /// locations. Each location object is converted into a ``Waypoint`` object. This class respects the `CLLocation` + /// class’s `coordinate` and `horizontalAccuracy` properties, converting them into the ``Waypoint`` class’s + /// ``Waypoint/coordinate`` and ``Waypoint/coordinateAccuracy`` properties, respectively. + /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. + /// ``ProfileIdentifier/automobile`` is used by default. + /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. + public convenience init( + locations: [CLLocation], + profileIdentifier: ProfileIdentifier? = nil, + queryItems: [URLQueryItem]? = nil + ) { + let waypoints = locations.map { Waypoint(location: $0) } + self.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) + } +#endif + + /// Initializes a route options object for routes between the given geographic coordinates and an optional profile + /// identifier. + /// - Parameters: + /// - coordinates: An array of geographic coordinates representing locations that the route should visit in + /// chronological order. The array should contain at least two locations (the source and destination) and at most 25 + /// locations. Each coordinate is converted into a ``Waypoint`` object. + /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. + /// ``ProfileIdentifier/automobile`` is used by default. + /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. + public convenience init( + coordinates: [LocationCoordinate2D], + profileIdentifier: ProfileIdentifier? = nil, + queryItems: [URLQueryItem]? = nil + ) { + let waypoints = coordinates.map { Waypoint(coordinate: $0) } + self.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) + } + + private enum CodingKeys: String, CodingKey { + case allowsUTurnAtWaypoint = "continue_straight" + case includesAlternativeRoutes = "alternatives" + case includesExitRoundaboutManeuver = "roundabout_exits" + case roadClassesToAvoid = "exclude" + case roadClassesToAllow = "include" + case refreshingEnabled = "enable_refresh" + case initialManeuverAvoidanceRadius = "avoid_maneuver_radius" + case maximumHeight = "max_height" + case maximumWidth = "max_width" + case maximumWeight = "max_weight" + case alleyPriority = "alley_bias" + case walkwayPriority = "walkway_bias" + case speed = "walking_speed" + case waypointTargets = "waypoint_targets" + case arriveBy = "arrive_by" + case departAt = "depart_at" + case layers + case includesTollPrices = "compute_toll_cost" + } + + override public func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(allowsUTurnAtWaypoint, forKey: .allowsUTurnAtWaypoint) + try container.encode(includesAlternativeRoutes, forKey: .includesAlternativeRoutes) + try container.encode(includesExitRoundaboutManeuver, forKey: .includesExitRoundaboutManeuver) + try container.encode(roadClassesToAvoid, forKey: .roadClassesToAvoid) + try container.encode(roadClassesToAllow, forKey: .roadClassesToAllow) + try container.encode(refreshingEnabled, forKey: .refreshingEnabled) + try container.encodeIfPresent(initialManeuverAvoidanceRadius, forKey: .initialManeuverAvoidanceRadius) + try container.encodeIfPresent(maximumHeight?.converted(to: .meters).value, forKey: .maximumHeight) + try container.encodeIfPresent(maximumWidth?.converted(to: .meters).value, forKey: .maximumWidth) + try container.encodeIfPresent(maximumWeight?.converted(to: .metricTons).value, forKey: .maximumWeight) + try container.encodeIfPresent(alleyPriority, forKey: .alleyPriority) + try container.encodeIfPresent(walkwayPriority, forKey: .walkwayPriority) + try container.encodeIfPresent(speed, forKey: .speed) + + let formatter = DateFormatter.ISO8601DirectionsFormatter() + if let arriveBy { + try container.encode(formatter.string(from: arriveBy), forKey: .arriveBy) + } + if let departAt { + try container.encode(formatter.string(from: departAt), forKey: .departAt) + } + + if includesTollPrices { + try container.encode(includesTollPrices, forKey: .includesTollPrices) + } + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.allowsUTurnAtWaypoint = try container.decode(Bool.self, forKey: .allowsUTurnAtWaypoint) + + self.includesAlternativeRoutes = try container.decode(Bool.self, forKey: .includesAlternativeRoutes) + + self.includesExitRoundaboutManeuver = try container.decode(Bool.self, forKey: .includesExitRoundaboutManeuver) + + self.roadClassesToAvoid = try container.decode(RoadClasses.self, forKey: .roadClassesToAvoid) + + self.roadClassesToAllow = try container.decode(RoadClasses.self, forKey: .roadClassesToAllow) + + self.refreshingEnabled = try container.decode(Bool.self, forKey: .refreshingEnabled) + + self._initialManeuverAvoidanceRadius = try container.decodeIfPresent( + LocationDistance.self, + forKey: .initialManeuverAvoidanceRadius + ) + + if let maximumHeightValue = try container.decodeIfPresent(Double.self, forKey: .maximumHeight) { + self.maximumHeight = Measurement(value: maximumHeightValue, unit: .meters) + } + + if let maximumWidthValue = try container.decodeIfPresent(Double.self, forKey: .maximumWidth) { + self.maximumWidth = Measurement(value: maximumWidthValue, unit: .meters) + } + if let maximumWeightValue = try container.decodeIfPresent(Double.self, forKey: .maximumWeight) { + self.maximumWeight = Measurement(value: maximumWeightValue, unit: .metricTons) + } + + self.alleyPriority = try container.decodeIfPresent(DirectionsPriority.self, forKey: .alleyPriority) + + self.walkwayPriority = try container.decodeIfPresent(DirectionsPriority.self, forKey: .walkwayPriority) + + self.speed = try container.decodeIfPresent(LocationSpeed.self, forKey: .speed) + + let formatter = DateFormatter.ISO8601DirectionsFormatter() + if let dateString = try container.decodeIfPresent(String.self, forKey: .departAt) { + self.departAt = formatter.date(from: dateString) + } + + if let dateString = try container.decodeIfPresent(String.self, forKey: .arriveBy) { + self.arriveBy = formatter.date(from: dateString) + } + + self.includesTollPrices = try container.decodeIfPresent(Bool.self, forKey: .includesTollPrices) ?? false + + try super.init(from: decoder) + } + + /// Initializes an equivalent route options object from a match options object. Desirable for building a navigation + /// experience from map matching. + /// + /// - Parameter matchOptions: The ``MatchOptions`` that is being used to convert to a ``RouteOptions`` object. + public convenience init(matchOptions: MatchOptions) { + self.init(waypoints: matchOptions.waypoints, profileIdentifier: matchOptions.profileIdentifier) + self.includesSteps = matchOptions.includesSteps + self.shapeFormat = matchOptions.shapeFormat + self.attributeOptions = matchOptions.attributeOptions + self.routeShapeResolution = matchOptions.routeShapeResolution + self.locale = matchOptions.locale + self.includesSpokenInstructions = matchOptions.includesSpokenInstructions + self.includesVisualInstructions = matchOptions.includesVisualInstructions + } + + override var abridgedPath: String { + return "directions/v5/\(profileIdentifier.rawValue)" + } + + // MARK: Influencing the Path of the Route + + /// A Boolean value that indicates whether a returned route may require a point U-turn at an intermediate waypoint. + /// + /// If the value of this property is `true`, a returned route may require an immediate U-turn at an intermediate + /// waypoint. At an intermediate waypoint, if the value of this property is `false`, each returned route may + /// continue straight ahead or turn to either side but may not U-turn. This property has no effect if only two + /// waypoints are specified. + /// + /// Set this property to `true` if you expect the user to traverse each leg of the trip separately. For example, it + /// would be quite easy for the user to effectively “U-turn” at a waypoint if the user first parks the car and + /// patronizes a restaurant there before embarking on the next leg of the trip. Set this property to `false` if you + /// expect the user to proceed to the next waypoint immediately upon arrival. For example, if the user only needs to + /// drop off a passenger or package at the waypoint before continuing, it would be inconvenient to perform a U-turn + /// at that location. + /// + /// The default value of this property is `false` when the profile identifier is ``ProfileIdentifier/automobile`` or + /// ``ProfileIdentifier/automobileAvoidingTraffic`` and `true` otherwise. + open var allowsUTurnAtWaypoint: Bool + + /// The route classes that the calculated routes will avoid. + /// + /// Currently, you can only specify a single road class to avoid. + open var roadClassesToAvoid: RoadClasses = [] + + /// The route classes that the calculated routes will allow. + /// + /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/automobile`` or + /// ``ProfileIdentifier/automobileAvoidingTraffic`` + open var roadClassesToAllow: RoadClasses = [] + + /// The number that influences whether the route should prefer or avoid alleys or narrow service roads between + /// buildings. + /// If this property isn't explicitly set, the Directions API will choose the most reasonable value. + /// + /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/automobile`` or + /// ``ProfileIdentifier/walking``. + /// + /// The value of this property must be at least ``DirectionsPriority/low`` and at most ``DirectionsPriority/high``. + /// ``DirectionsPriority/medium`` neither prefers nor avoids alleys, while a negative value between + /// ``DirectionsPriority/low`` and ``DirectionsPriority/medium`` avoids alleys, and a positive value between + /// ``DirectionsPriority/medium`` and ``DirectionsPriority/high`` prefers alleys. A value of 0.9 is suitable for + /// pedestrians who are comfortable with walking down alleys. + open var alleyPriority: DirectionsPriority? + + /// The number that influences whether the route should prefer or avoid roads or paths that are set aside for + /// pedestrian-only use (walkways or footpaths). + /// If this property isn't explicitly set, the Directions API will choose the most reasonable value. + /// + /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/walking``. You can + /// adjust this property to avoid [sidewalks and crosswalks that are mapped as separate + /// footpaths](https://wiki.openstreetmap.org/wiki/Sidewalks#Sidewalk_as_separate_way), which may be more granular + /// than needed for some forms of pedestrian navigation. + /// + /// The value of this property must be at least ``DirectionsPriority/low`` and at most ``DirectionsPriority/high``. + /// ``DirectionsPriority/medium`` neither prefers nor avoids walkways, while a negative value between + /// ``DirectionsPriority/low`` and ``DirectionsPriority/medium`` avoids walkways, and a positive value between + /// ``DirectionsPriority/medium`` and ``DirectionsPriority/high`` prefers walkways. A value of −0.1 results in less + /// verbose routes in cities where sidewalks and crosswalks are generally mapped as separate footpaths. + open var walkwayPriority: DirectionsPriority? + + /// The expected uniform travel speed measured in meters per second. + /// If this property isn't explicitly set, the Directions API will choose the most reasonable value. + /// + /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/walking``. You can + /// adjust this property to account for running or for faster or slower gaits. When the profile identifier is set to + /// another profile identifier, such as ``ProfileIdentifier/automobile`, this property is ignored in favor of the + /// expected travel speed on each road along the route. This property may be supported by other routing profiles in + /// the future. + /// + /// The value of this property must be at least `CLLocationSpeed.minimumWalking` and at most + /// `CLLocationSpeed.maximumWalking`. `CLLocationSpeed.normalWalking` corresponds to a typical preferred walking + /// speed. + open var speed: LocationSpeed? + + /// The desired arrival time, ignoring seconds precision, in the local time at the route destination. + /// + /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/automobile``. + open var arriveBy: Date? + + /// The desired departure time, ignoring seconds precision, in the local time at the route origin + /// + /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/automobile`` or + /// ``ProfileIdentifier/automobileAvoidingTraffic``. + open var departAt: Date? + + // MARK: Specifying the Response Format + + /// A Boolean value indicating whether alternative routes should be included in the response. + /// + /// If the value of this property is `false`, the server only calculates a single route that visits each of the + /// waypoints. If the value of this property is `true`, the server attempts to find additional reasonable routes + /// that visit the waypoints. Regardless, multiple routes are only returned if it is possible to visit the waypoints + /// by a different route without significantly increasing the distance or travel time. The alternative routes may + /// partially overlap with the preferred route, especially if intermediate waypoints are specified. + /// + /// Alternative routes may take longer to calculate and make the response significantly larger, so only request + /// alternative routes if you intend to display them to the user or let the user choose them over the preferred + /// route. For example, do not request alternative routes if you only want to know the distance or estimated travel + /// time to a destination. + /// + /// The default value of this property is `false`. + open var includesAlternativeRoutes = false + + /// A Boolean value indicating whether the route includes a ``ManeuverType/exitRoundabout`` or + /// ``ManeuverType/exitRotary`` step when traversing a roundabout or rotary, respectively. + /// + /// If this option is set to `true`, a route that traverses a roundabout includes both a + /// ``ManeuverType/takeRoundabout`` step and a ``ManeuverType/exitRoundabout`` step; likewise, a route that + /// traverses a large, named roundabout includes both a ``ManeuverType/takeRotary`` step and a + /// ``ManeuverType/exitRotary`` step. Otherwise, it only includes a ``ManeuverType/takeRoundabout`` or + /// ``ManeuverType/takeRotary`` step. This option is set to `false` by default. + open var includesExitRoundaboutManeuver = false + + /// A Boolean value indicating whether `Directions` can refresh time-dependent properties of the ``RouteLeg``s of + /// the resulting ``Route``s. + /// + /// To refresh the ``RouteLeg/expectedSegmentTravelTimes``, ``RouteLeg/segmentSpeeds``, and + /// ``RouteLeg/segmentCongestionLevels`` properties, use the + /// `Directions.refreshRoute(responseIdentifier:routeIndex:fromLegAtIndex:completionHandler:)` method. This property + /// is ignored unless ``DirectionsOptions/profileIdentifier`` is ``ProfileIdentifier/automobileAvoidingTraffic``. + /// This option is set + /// to `false` by default. + open var refreshingEnabled = false + + /// The maximum vehicle height. + /// + /// If this parameter is provided, `Directions` will compute a route that includes only roads with a height limit + /// greater than or equal to the max vehicle height or no height limit. + /// + /// This property is supported by ``ProfileIdentifier/automobile`` and + /// ``ProfileIdentifier/automobileAvoidingTraffic`` profiles. + /// The value must be between 0 and 10 when converted to meters. + open var maximumHeight: Measurement? + + /// The maximum vehicle width. + /// + /// If this parameter is provided, `Directions` will compute a route that includes only roads with a width limit + /// greater than or equal to the max vehicle width or no width limit. + /// This property is supported by ``ProfileIdentifier/automobile`` and + /// ``ProfileIdentifier/automobileAvoidingTraffic`` profiles. + /// The value must be between 0 and 10 when converted to meters. + open var maximumWidth: Measurement? + + /// The maximum vehicle weight. + /// + /// If this parameter is provided, the `Directions` will compute a route that includes only roads with a weight + /// limit greater than or equal to the max vehicle weight. + /// This property is supported by ``ProfileIdentifier/automobile`` and + /// ``ProfileIdentifier/automobileAvoidingTraffic`` profiles. + /// The value must be between 0 and 100 metric tons. If unspecified, 2.5 metric tons is assumed. + open var maximumWeight: Measurement? + /// A radius around the starting point in which the API will avoid returning any significant maneuvers. + /// + /// Use this option when the vehicle is traveling at a significant speed to avoid dangerous maneuvers when + /// re-routing. If a route is not found using the specified value, it will be ignored. Note that if a large radius + /// is used, the API may ignore an important turn and return a long straight path before the first maneuver. + /// + /// This value is clamped to `LocationDistance.minimumManeuverIgnoringRadius` and + /// `LocationDistance.maximumManeuverIgnoringRadius`. + open var initialManeuverAvoidanceRadius: LocationDistance? { + get { + _initialManeuverAvoidanceRadius + } + set { + _initialManeuverAvoidanceRadius = newValue.map { + min( + LocationDistance.maximumManeuverIgnoringRadius, + max( + LocationDistance.minimumManeuverIgnoringRadius, + $0 + ) + ) + } + } + } + + private var _initialManeuverAvoidanceRadius: LocationDistance? + + /// Toggle whether to return calculated toll cost for the route, if data is available. + /// + /// Toll prices are populeted in resulting route's ``Route/tollPrices``. + /// Default value is `false`. + open var includesTollPrices = false + + // MARK: Getting the Request URL + + override open var urlQueryItems: [URLQueryItem] { + var params: [URLQueryItem] = [ + URLQueryItem( + name: CodingKeys.includesAlternativeRoutes.stringValue, + value: includesAlternativeRoutes.queryString + ), + URLQueryItem( + name: CodingKeys.allowsUTurnAtWaypoint.stringValue, + value: (!allowsUTurnAtWaypoint).queryString + ), + ] + + if includesExitRoundaboutManeuver { + params.append(URLQueryItem( + name: CodingKeys.includesExitRoundaboutManeuver.stringValue, + value: includesExitRoundaboutManeuver.queryString + )) + } + if let alleyPriority = alleyPriority?.rawValue { + params.append(URLQueryItem(name: CodingKeys.alleyPriority.stringValue, value: String(alleyPriority))) + } + + if let walkwayPriority = walkwayPriority?.rawValue { + params.append(URLQueryItem(name: CodingKeys.walkwayPriority.stringValue, value: String(walkwayPriority))) + } + + if let speed { + params.append(URLQueryItem(name: CodingKeys.speed.stringValue, value: String(speed))) + } + + if !roadClassesToAvoid.isEmpty { + let roadClasses = roadClassesToAvoid.description + params.append(URLQueryItem(name: CodingKeys.roadClassesToAvoid.stringValue, value: roadClasses)) + } + + if !roadClassesToAllow.isEmpty { + let parameterValue = roadClassesToAllow.description + params.append(URLQueryItem(name: CodingKeys.roadClassesToAllow.stringValue, value: parameterValue)) + } + + if refreshingEnabled, profileIdentifier == .automobileAvoidingTraffic { + params.append(URLQueryItem( + name: CodingKeys.refreshingEnabled.stringValue, + value: refreshingEnabled.queryString + )) + } + + if waypoints.first(where: { $0.targetCoordinate != nil }) != nil { + let targetCoordinates = waypoints.filter(\.separatesLegs) + .map { $0.targetCoordinate?.requestDescription ?? "" }.joined(separator: ";") + params.append(URLQueryItem(name: CodingKeys.waypointTargets.stringValue, value: targetCoordinates)) + } + + if waypoints.contains(where: { $0.layer != nil }) { + let layers = waypoints.map { $0.layer?.description ?? "" }.joined(separator: ";") + params.append(URLQueryItem(name: CodingKeys.layers.stringValue, value: layers)) + } + + if let initialManeuverAvoidanceRadius { + params.append(URLQueryItem( + name: CodingKeys.initialManeuverAvoidanceRadius.stringValue, + value: String(initialManeuverAvoidanceRadius) + )) + } + + if let maximumHeight { + let heightInMeters = maximumHeight.converted(to: .meters).value + params.append(URLQueryItem(name: CodingKeys.maximumHeight.stringValue, value: String(heightInMeters))) + } + + if let maximumWidth { + let widthInMeters = maximumWidth.converted(to: .meters).value + params.append(URLQueryItem(name: CodingKeys.maximumWidth.stringValue, value: String(widthInMeters))) + } + + if let maximumWeight { + let weightInTonnes = maximumWeight.converted(to: .metricTons).value + params.append(URLQueryItem(name: CodingKeys.maximumWeight.stringValue, value: String(weightInTonnes))) + } + + if [ProfileIdentifier.automobile, .automobileAvoidingTraffic].contains(profileIdentifier) { + let formatter = DateFormatter.ISO8601DirectionsFormatter() + + if let departAt { + params.append(URLQueryItem( + name: CodingKeys.departAt.stringValue, + value: String(formatter.string(from: departAt)) + )) + } + + if profileIdentifier == .automobile, + let arriveBy + { + params.append(URLQueryItem( + name: CodingKeys.arriveBy.stringValue, + value: String(formatter.string(from: arriveBy)) + )) + } + } + + if includesTollPrices { + params.append(URLQueryItem( + name: CodingKeys.includesTollPrices.stringValue, + value: includesTollPrices.queryString + )) + } + + return params + super.urlQueryItems + } +} + +@available(*, unavailable) +extension RouteOptions: @unchecked Sendable {} + +extension Bool { + var queryString: String { + return self ? "true" : "false" + } +} + +extension LocationSpeed { + /// Pedestrians are assumed to walk at an average rate of 1.42 meters per second (5.11 kilometers per hour or 3.18 + /// miles per hour), corresponding to a typical preferred walking speed. + static let normalWalking: LocationSpeed = 1.42 + + /// Pedestrians are assumed to walk no slower than 0.14 meters per second (0.50 kilometers per hour or 0.31 miles + /// per hour) on average. + static let minimumWalking: LocationSpeed = 0.14 + + /// Pedestrians are assumed to walk no faster than 6.94 meters per second (25.0 kilometers per hour or 15.5 miles + /// per hour) on average. + static let maximumWalking: LocationSpeed = 6.94 +} + +extension LocationDistance { + /// Minimum positive value to ignore maneuvers around origin point during routing. + static let minimumManeuverIgnoringRadius: LocationDistance = 1 + + /// Maximum value to ignore maneuvers around origin point during routing. + static let maximumManeuverIgnoringRadius: LocationDistance = 1000 +} + +extension DateFormatter { + /// Special ISO8601 date converter for `depart_at` and `arrive_by` parameters, as Directions API explicitly require + /// no seconds bit. + fileprivate static func ISO8601DirectionsFormatter() -> DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm" + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteRefreshResponse.swift b/ios/Classes/Navigation/MapboxDirections/RouteRefreshResponse.swift new file mode 100644 index 000000000..5d17cb383 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RouteRefreshResponse.swift @@ -0,0 +1,88 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import Turf + +/// A Directions Refresh API response. +public struct RouteRefreshResponse: ForeignMemberContainer, Equatable { + public var foreignMembers: JSONObject = [:] + + /// The raw HTTP response from the Directions Refresh API. + public let httpResponse: HTTPURLResponse? + + /// The response identifier used to request the refreshed route. + public let identifier: String + + /// The route index used to request the refreshed route. + public var routeIndex: Int + + public var startLegIndex: Int + + /// A skeleton route that contains only the time-sensitive information that has been updated. + public var route: RefreshedRoute + + /// The credentials used to make the request. + public let credentials: Credentials + + /// The time when this ``RouteRefreshResponse`` object was created, which is immediately upon recieving the raw URL + /// response. + /// + /// If you manually start fetching a task returned by + /// `Directions.urlRequest(forRefreshing:routeIndex:currentLegIndex:)`, this property is set to `nil`; use the + /// `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be set to `nil` if + /// you create this result from a JSON object or encoded object. + /// + /// This property does not persist after encoding and decoding. + public var created = Date() +} + +extension RouteRefreshResponse: Codable { + enum CodingKeys: String, CodingKey { + case identifier = "uuid" + case route + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse + + guard let credentials = decoder.userInfo[.credentials] as? Credentials else { + throw DirectionsCodingError.missingCredentials + } + + self.credentials = credentials + + if let identifier = decoder.userInfo[.responseIdentifier] as? String { + self.identifier = identifier + } else { + throw DirectionsCodingError.missingOptions + } + + self.route = try container.decode(RefreshedRoute.self, forKey: .route) + + if let routeIndex = decoder.userInfo[.routeIndex] as? Int { + self.routeIndex = routeIndex + } else { + throw DirectionsCodingError.missingOptions + } + + if let startLegIndex = decoder.userInfo[.startLegIndex] as? Int { + self.startLegIndex = startLegIndex + } else { + throw DirectionsCodingError.missingOptions + } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(identifier, forKey: .identifier) + + try container.encode(route, forKey: .route) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteRefreshSource.swift b/ios/Classes/Navigation/MapboxDirections/RouteRefreshSource.swift new file mode 100644 index 000000000..08e9ce0d1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RouteRefreshSource.swift @@ -0,0 +1,63 @@ +import Foundation + +/// A skeletal route containing infromation to refresh ``Route`` object attributes. +public protocol RouteRefreshSource { + var refreshedLegs: [RouteLegRefreshSource] { get } +} + +/// A skeletal route leg containing infromation to refresh ``RouteLeg`` object attributes. +public protocol RouteLegRefreshSource { + var refreshedAttributes: RouteLeg.Attributes { get } + var refreshedIncidents: [Incident]? { get } + var refreshedClosures: [RouteLeg.Closure]? { get } +} + +extension RouteLegRefreshSource { + public var refreshedIncidents: [Incident]? { + return nil + } + + public var refreshedClosures: [RouteLeg.Closure]? { + return nil + } +} + +extension Route: RouteRefreshSource { + public var refreshedLegs: [RouteLegRefreshSource] { + legs + } +} + +extension RouteLeg: RouteLegRefreshSource { + public var refreshedAttributes: Attributes { + attributes + } + + public var refreshedIncidents: [Incident]? { + incidents + } + + public var refreshedClosures: [RouteLeg.Closure]? { + closures + } +} + +extension RefreshedRoute: RouteRefreshSource { + public var refreshedLegs: [RouteLegRefreshSource] { + legs + } +} + +extension RefreshedRouteLeg: RouteLegRefreshSource { + public var refreshedAttributes: RouteLeg.Attributes { + attributes + } + + public var refreshedIncidents: [Incident]? { + incidents + } + + public var refreshedClosures: [RouteLeg.Closure]? { + closures + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteResponse.swift b/ios/Classes/Navigation/MapboxDirections/RouteResponse.swift new file mode 100644 index 000000000..212a41052 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RouteResponse.swift @@ -0,0 +1,393 @@ +import Foundation +import Turf +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public enum ResponseOptions: Sendable { + case route(RouteOptions) + case match(MatchOptions) +} + +@available(*, unavailable) +extension ResponseOptions: @unchecked Sendable {} + +/// A ``RouteResponse`` object is a structure that corresponds to a directions response returned by the Mapbox +/// Directions API. +public struct RouteResponse: ForeignMemberContainer { + public var foreignMembers: JSONObject = [:] + + /// The raw HTTP response from the Directions API. + public let httpResponse: HTTPURLResponse? + + /// The unique identifier that the Mapbox Directions API has assigned to this response. + public let identifier: String? + + /// An array of ``Route`` objects sorted from most recommended to least recommended. A route may be highly + /// recommended + /// based on characteristics such as expected travel time or distance. + /// This property contains a maximum of two ``Route``s. + public var routes: [Route]? { + didSet { + updateRoadClassExclusionViolations() + } + } + + /// An array of ``Waypoint`` objects in the order of the input coordinates. Each ``Waypoint`` is an input coordinate + /// snapped to the road and path network. + /// + /// This property omits the waypoint corresponding to any waypoint in ``DirectionsOptions/waypoints`` that has + /// ``Waypoint/separatesLegs`` set to `true`. + public let waypoints: [Waypoint]? + + /// The criteria for the directions response. + public let options: ResponseOptions + + /// The credentials used to make the request. + public let credentials: Credentials + + /// The time when this ``RouteResponse`` object was created, which is immediately upon recieving the raw URL + /// response. + /// + /// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to + /// `nil`; use the `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be + /// set to `nil` if you create this result from a JSON object or encoded object. + /// + /// This property does not persist after encoding and decoding. + public var created: Date = .init() + + /// A time period during which the routes from this ``RouteResponse`` are eligable for refreshing. + /// + /// `nil` value indicates that route refreshing is not available for related routes. + public let refreshTTL: TimeInterval? + + /// A deadline after which the routes from this ``RouteResponse`` are eligable for refreshing. + /// + /// `nil` value indicates that route refreshing is not available for related routes. + public var refreshInvalidationDate: Date? { + refreshTTL.map { created.addingTimeInterval($0) } + } + + /// Managed array of ``RoadClasses`` restrictions specified to ``RouteOptions/roadClassesToAvoid`` which were + /// violated + /// during route calculation. + /// + /// Routing engine may still utilize ``RoadClasses`` meant to be avoided in cases when routing is impossible + /// otherwise. + /// + /// Violations are ordered by routes from the ``routes`` array, then by a leg, step, and intersection, where + /// ``RoadClasses`` restrictions were ignored. `nil` and empty return arrays correspond to `nil` and empty + /// ``routes`` + /// array respectively. + public private(set) var roadClassExclusionViolations: [RoadClassExclusionViolation]? +} + +extension RouteResponse: Codable { + enum CodingKeys: String, CodingKey { + case message + case error + case identifier = "uuid" + case routes + case waypoints + case refreshTTL = "refresh_ttl" + } + + public init( + httpResponse: HTTPURLResponse?, + identifier: String? = nil, + routes: [Route]? = nil, + waypoints: [Waypoint]? = nil, + options: ResponseOptions, + credentials: Credentials, + refreshTTL: TimeInterval? = nil + ) { + self.httpResponse = httpResponse + self.identifier = identifier + self.options = options + self.routes = routes + self.waypoints = waypoints + self.credentials = credentials + self.refreshTTL = refreshTTL + + updateRoadClassExclusionViolations() + } + + public init(matching response: MapMatchingResponse, options: MatchOptions, credentials: Credentials) throws { + let decoder = JSONDecoder() + let encoder = JSONEncoder() + + decoder.userInfo[.options] = options + decoder.userInfo[.credentials] = credentials + encoder.userInfo[.options] = options + encoder.userInfo[.credentials] = credentials + + var routes: [Route]? + + if let matches = response.matches { + let matchesData = try encoder.encode(matches) + routes = try decoder.decode([Route].self, from: matchesData) + } + + var waypoints: [Waypoint]? + + if let tracepoints = response.tracepoints { + let filtered = tracepoints.compactMap { $0 } + let tracepointsData = try encoder.encode(filtered) + waypoints = try decoder.decode([Waypoint].self, from: tracepointsData) + } + + self.init( + httpResponse: response.httpResponse, + identifier: nil, + routes: routes, + waypoints: waypoints, + options: .match(options), + credentials: credentials + ) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse + + guard let credentials = decoder.userInfo[.credentials] as? Credentials else { + throw DirectionsCodingError.missingCredentials + } + + self.credentials = credentials + + if let options = decoder.userInfo[.options] as? RouteOptions { + self.options = .route(options) + } else if let options = decoder.userInfo[.options] as? MatchOptions { + self.options = .match(options) + } else { + throw DirectionsCodingError.missingOptions + } + + self.identifier = try container.decodeIfPresent(String.self, forKey: .identifier) + + // Decode waypoints from the response and update their names according to the waypoints from + // DirectionsOptions.waypoints. + let decodedWaypoints = try container.decodeIfPresent([Waypoint?].self, forKey: .waypoints)?.compactMap { $0 } + var optionsWaypoints: [Waypoint] = [] + + switch options { + case .match(options: let matchOpts): + optionsWaypoints = matchOpts.waypoints + case .route(options: let routeOpts): + optionsWaypoints = routeOpts.waypoints + } + + if let decodedWaypoints { + // The response lists the same number of tracepoints as the waypoints in the request, whether or not a given + // waypoint is leg-separating. + var waypoints = zip(decodedWaypoints, optionsWaypoints).map { pair -> Waypoint in + let (decodedWaypoint, waypointInOptions) = pair + var waypoint = Waypoint( + coordinate: decodedWaypoint.coordinate, + coordinateAccuracy: waypointInOptions.coordinateAccuracy, + name: waypointInOptions.name?.nonEmptyString ?? decodedWaypoint.name + ) + waypoint.snappedDistance = decodedWaypoint.snappedDistance + waypoint.targetCoordinate = waypointInOptions.targetCoordinate + waypoint.heading = waypointInOptions.heading + waypoint.headingAccuracy = waypointInOptions.headingAccuracy + waypoint.separatesLegs = waypointInOptions.separatesLegs + waypoint.allowsArrivingOnOppositeSide = waypointInOptions.allowsArrivingOnOppositeSide + + waypoint.foreignMembers = decodedWaypoint.foreignMembers + + return waypoint + } + + if waypoints.startIndex < waypoints.endIndex { + waypoints[waypoints.startIndex].separatesLegs = true + } + let lastIndex = waypoints.endIndex - 1 + if waypoints.indices.contains(lastIndex) { + waypoints[lastIndex].separatesLegs = true + } + + self.waypoints = waypoints + } else { + self.waypoints = decodedWaypoints + } + + if var routes = try container.decodeIfPresent([Route].self, forKey: .routes) { + // Postprocess each route. + for routeIndex in routes.indices { + // Imbue each route’s legs with the waypoints refined above. + if let waypoints { + routes[routeIndex].legSeparators = waypoints.filter(\.separatesLegs) + } + } + self.routes = routes + } else { + self.routes = nil + } + + self.refreshTTL = try container.decodeIfPresent(TimeInterval.self, forKey: .refreshTTL) + + updateRoadClassExclusionViolations() + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(identifier, forKey: .identifier) + try container.encodeIfPresent(routes, forKey: .routes) + try container.encodeIfPresent(waypoints, forKey: .waypoints) + try container.encodeIfPresent(refreshTTL, forKey: .refreshTTL) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } +} + +extension RouteResponse { + mutating func updateRoadClassExclusionViolations() { + guard case .route(let routeOptions) = options else { + roadClassExclusionViolations = nil + return + } + + guard let routes else { + roadClassExclusionViolations = nil + return + } + + let avoidedClasses = routeOptions.roadClassesToAvoid + + guard !avoidedClasses.isEmpty else { + roadClassExclusionViolations = nil + return + } + + var violations = [RoadClassExclusionViolation]() + + for (routeIndex, route) in routes.enumerated() { + for (legIndex, leg) in route.legs.enumerated() { + for (stepIndex, step) in leg.steps.enumerated() { + for (intersectionIndex, intersection) in (step.intersections ?? []).enumerated() { + if let outletRoadClasses = intersection.outletRoadClasses, + !avoidedClasses.isDisjoint(with: outletRoadClasses) + { + violations.append(RoadClassExclusionViolation( + roadClasses: avoidedClasses.intersection(outletRoadClasses), + routeIndex: routeIndex, + legIndex: legIndex, + stepIndex: stepIndex, + intersectionIndex: intersectionIndex + )) + } + } + } + } + } + roadClassExclusionViolations = violations + } + + /// Filters ``roadClassExclusionViolations`` lazily to search for specific leg and step. + /// + /// - parameter routeIndex: Index of a route inside current ``RouteResponse`` to search in. + /// - parameter legIndex: Index of a leg inside related ``Route``to search in. + /// - returns: Lazy filtered array of ``RoadClassExclusionViolation`` under given indicies. + /// + /// Passing `nil` as `legIndex` will result in searching for all legs. + public func exclusionViolations( + routeIndex: Int, + legIndex: Int? = nil + ) -> LazyFilterSequence<[RoadClassExclusionViolation]> { + return filteredViolations( + routeIndex: routeIndex, + legIndex: legIndex, + stepIndex: nil, + intersectionIndex: nil + ) + } + + /// Filters ``roadClassExclusionViolations`` lazily to search for specific leg and step. + /// + /// - parameter routeIndex: Index of a route inside current ``RouteResponse`` to search in. + /// - parameter legIndex: Index of a leg inside related ``Route``to search in. + /// - parameter stepIndex: Index of a step inside given ``Route``'s leg. + /// - returns: Lazy filtered array of ``RoadClassExclusionViolation`` under given indicies. + /// + /// Passing `nil` as `stepIndex` will result in searching for all steps. + public func exclusionViolations( + routeIndex: Int, + legIndex: Int, + stepIndex: Int? = nil + ) -> LazyFilterSequence<[RoadClassExclusionViolation]> { + return filteredViolations( + routeIndex: routeIndex, + legIndex: legIndex, + stepIndex: stepIndex, + intersectionIndex: nil + ) + } + + /// Filters ``roadClassExclusionViolations`` lazily to search for specific leg, step and intersection. + /// + /// - parameter routeIndex: Index of a route inside current ``RouteResponse`` to search in. + /// - parameter legIndex: Index of a leg inside related ``Route``to search in. + /// - parameter stepIndex: Index of a step inside given ``Route``'s leg. + /// - parameter intersectionIndex: Index of an intersection inside given ``Route``'s leg and step. + /// - returns: Lazy filtered array of ``RoadClassExclusionViolation`` under given indicies. + /// + /// Passing `nil` as `intersectionIndex` will result in searching for all intersections of given step. + public func exclusionViolations( + routeIndex: Int, + legIndex: Int, + stepIndex: Int, + intersectionIndex: Int? + ) -> LazyFilterSequence<[RoadClassExclusionViolation]> { + return filteredViolations( + routeIndex: routeIndex, + legIndex: legIndex, + stepIndex: stepIndex, + intersectionIndex: intersectionIndex + ) + } + + private func filteredViolations( + routeIndex: Int, + legIndex: Int? = nil, + stepIndex: Int? = nil, + intersectionIndex: Int? = nil + ) -> LazyFilterSequence<[RoadClassExclusionViolation]> { + assert( + !(stepIndex == nil && intersectionIndex != nil), + "It is forbidden to select `intersectionIndex` without specifying `stepIndex`." + ) + + guard let roadClassExclusionViolations else { + return LazyFilterSequence<[RoadClassExclusionViolation]>(_base: [], { _ in true }) + } + + var filtered = roadClassExclusionViolations.lazy.filter { + $0.routeIndex == routeIndex + } + + if let legIndex { + filtered = filtered.filter { + $0.legIndex == legIndex + } + } + + if let stepIndex { + filtered = filtered.filter { + $0.stepIndex == stepIndex + } + } + + if let intersectionIndex { + filtered = filtered.filter { + $0.intersectionIndex == intersectionIndex + } + } + + return filtered + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteStep.swift b/ios/Classes/Navigation/MapboxDirections/RouteStep.swift new file mode 100644 index 000000000..7226521f0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/RouteStep.swift @@ -0,0 +1,1112 @@ +import Foundation +import Turf +#if canImport(CoreLocation) +import CoreLocation +#endif + +/// A ``TransportType`` specifies the mode of transportation used for part of a route. +public enum TransportType: String, Codable, Equatable, Sendable { + /// Possible transport types when the `profileIdentifier` is ``ProfileIdentifier/automobile`` or + /// ``ProfileIdentifier/automobileAvoidingTraffic`` + + /// The route requires the user to drive or ride a car, truck, or motorcycle. + /// This is the usual transport type when the `profileIdentifier` is ``ProfileIdentifier/automobile`` or + /// ``ProfileIdentifier/automobileAvoidingTraffic``. + case automobile = "driving" // automobile + + /// The route requires the user to board a ferry. + /// + /// The user should verify that the ferry is in operation. For driving and cycling directions, the user should also + /// verify that their vehicle is permitted onboard the ferry. + case ferry // automobile, walking, cycling + + /// The route requires the user to cross a movable bridge. + /// + /// The user may need to wait for the movable bridge to become passable before continuing. + case movableBridge = "movable bridge" // automobile, cycling + + /// The route becomes impassable at this point. + /// + /// You should not encounter this transport type under normal circumstances. + case inaccessible = "unaccessible" // automobile, walking, cycling + + /// Possible transport types when the `profileIdentifier` is ``ProfileIdentifier/walking`` + + /// The route requires the user to walk. + /// + /// This is the usual transport type when the `profileIdentifier` is ``ProfileIdentifier/walking``. For cycling + /// directions, this value indicates that the user is expected to dismount. + case walking // walking, cycling + + /// Possible transport types when the `profileIdentifier` is ``ProfileIdentifier/cycling`` + + /// The route requires the user to ride a bicycle. + /// + /// This is the usual transport type when the `profileIdentifier` is ``ProfileIdentifier/cycling``. + case cycling // cycling + + /// The route requires the user to board a train. + /// + /// The user should consult the train’s timetable. For cycling directions, the user should also verify that bicycles + /// are permitted onboard the train. + case train // cycling + + /// Custom implementation of decoding is needed to circumvent issue reported in + /// https://github.com/mapbox/mapbox-directions-swift/issues/413 + public init(from decoder: Decoder) throws { + let valueContainer = try decoder.singleValueContainer() + let rawValue = try valueContainer.decode(String.self) + + if rawValue == "pushing bike" { + self = .walking + + return + } + + guard let value = TransportType(rawValue: rawValue) else { + throw DecodingError.dataCorruptedError( + in: valueContainer, + debugDescription: "Cannot initialize TransportType from invalid String value \(rawValue)" + ) + } + + self = value + } +} + +/// A ``ManeuverType`` specifies the type of maneuver required to complete the route step. You can pair a maneuver type +/// with a ``ManeuverDirection`` to choose an appropriate visual or voice prompt to present the user. +/// +/// To avoid a complex series of if-else-if statements or switch statements, use pattern matching with a single switch +/// statement on a tuple that consists of the maneuver type and maneuver direction. +public enum ManeuverType: String, Codable, Equatable, Sendable { + /// The step requires the user to depart from a waypoint. + /// + /// If the waypoint is some distance away from the nearest road, the maneuver direction indicates the direction the + /// user must turn upon reaching the road. + case depart + + /// The step requires the user to turn. + /// + /// The maneuver direction indicates the direction in which the user must turn relative to the current direction of + /// travel. The exit index indicates the number of intersections, large or small, from the previous maneuver up to + /// and including the intersection at which the user must turn. + case turn + + /// The step requires the user to continue after a turn. + case `continue` + + /// The step requires the user to continue on the current road as it changes names. + /// + /// The step’s name contains the road’s new name. To get the road’s old name, use the previous step’s name. + case passNameChange = "new name" + + /// The step requires the user to merge onto another road. + /// + /// The maneuver direction indicates the side from which the other road approaches the intersection relative to the + /// user. + case merge + + /// The step requires the user to take a entrance ramp (slip road) onto a highway. + case takeOnRamp = "on ramp" + + /// The step requires the user to take an exit ramp (slip road) off a highway. + /// + /// The maneuver direction indicates the side of the highway from which the user must exit. The exit index indicates + /// the number of highway exits from the previous maneuver up to and including the exit that the user must take. + case takeOffRamp = "off ramp" + + /// The step requires the user to choose a fork at a Y-shaped fork in the road. + /// + /// The maneuver direction indicates which fork to take. + case reachFork = "fork" + + /// The step requires the user to turn at either a T-shaped three-way intersection or a sharp bend in the road where + /// the road also changes names. + /// + /// This maneuver type is called out separately so that the user may be able to proceed more confidently, without + /// fear of having overshot the turn. If this distinction is unimportant to you, you may treat the maneuver as an + /// ordinary ``ManeuverType/turn``. + case reachEnd = "end of road" + + /// The step requires the user to get into a specific lane in order to continue along the current road. + /// + /// The maneuver direction is set to ``ManeuverDirection/straightAhead``. Each of the first intersection’s usable + /// approach lanes also has an indication of ``LaneIndication/straightAhead``. A maneuver in a different direction + /// would instead have a maneuver type of ``ManeuverType/turn``. + /// + /// This maneuver type is called out separately so that the application can present the user with lane guidance + /// based on the first element in the ``RouteStep/intersections`` property. If lane guidance is unimportant to you, + /// you may + /// treat the maneuver as an ordinary ``ManeuverType/continue`` or ignore it. + case useLane = "use lane" + + /// The step requires the user to enter and traverse a roundabout (traffic circle or rotary). + /// + /// The step has no name, but the exit name is the name of the road to take to exit the roundabout. The exit index + /// indicates the number of roundabout exits up to and including the exit to take. + /// + /// If ``RouteOptions/includesExitRoundaboutManeuver`` is set to `true`, this step is followed by an + /// ``ManeuverType/exitRoundabout`` maneuver. Otherwise, this step represents the entire roundabout maneuver, from + /// the entrance to the exit. + case takeRoundabout = "roundabout" + + /// The step requires the user to enter and traverse a large, named roundabout (traffic circle or rotary). + /// + /// The step’s name is the name of the roundabout. The exit name is the name of the road to take to exit the + /// roundabout. The exit index indicates the number of rotary exits up to and including the exit that the user must + /// take. + /// + /// If ``RouteOptions/includesExitRoundaboutManeuver`` is set to `true`, this step is followed by an + /// ``ManeuverType/exitRotary`` maneuver. Otherwise, this step represents the entire roundabout maneuver, from the + /// entrance to the exit. + case takeRotary = "rotary" + + /// The step requires the user to enter and exit a roundabout (traffic circle or rotary) that is compact enough to + /// constitute a single intersection. + /// + /// The step’s name is the name of the road to take after exiting the roundabout. This maneuver type is called out + /// separately because the user may perceive the roundabout as an ordinary intersection with an island in the + /// middle. If this distinction is unimportant to you, you may treat the maneuver as either an ordinary + /// ``ManeuverType/turn`` or as a ``ManeuverType/takeRoundabout``. + case turnAtRoundabout = "roundabout turn" + + /// The step requires the user to exit a roundabout (traffic circle or rotary). + /// + /// This maneuver type follows a ``ManeuverType/takeRoundabout`` maneuver. It is only used when + /// ``RouteOptions/includesExitRoundaboutManeuver`` is set to true. + case exitRoundabout = "exit roundabout" + + /// The step requires the user to exit a large, named roundabout (traffic circle or rotary). + /// + /// This maneuver type follows a ``ManeuverType/takeRotary`` maneuver. It is only used when + /// ``RouteOptions/includesExitRoundaboutManeuver`` is set to true. + case exitRotary = "exit rotary" + + /// The step requires the user to respond to a change in travel conditions. + /// + /// This maneuver type may occur for example when driving directions require the user to board a ferry, or when + /// cycling directions require the user to dismount. The step’s transport type and instructions contains important + /// contextual details that should be presented to the user at the maneuver location. + /// + /// Similar changes can occur simultaneously with other maneuvers, such as when the road changes its name at the + /// site of a movable bridge. In such cases, ``heedWarning`` is suppressed in favor of another maneuver type. + case heedWarning = "notification" + + /// The step requires the user to arrive at a waypoint. + /// + /// The distance and expected travel time for this step are set to zero, indicating that the route or route leg is + /// complete. The maneuver direction indicates the side of the road on which the waypoint can be found (or whether + /// it is straight ahead). + case arrive + + // Unrecognized maneuver types are interpreted as turns. + // http://project-osrm.org/docs/v5.5.1/api/#stepmaneuver-object + static let `default` = ManeuverType.turn +} + +/// A ``ManeuverDirection`` clarifies a ``ManeuverType`` with directional information. The exact meaning of the maneuver +/// direction for a given step depends on the step’s maneuver type; see the ``ManeuverType`` documentation for details. +public enum ManeuverDirection: String, Codable, Equatable, Sendable { + /// The maneuver requires a sharp turn to the right. + case sharpRight = "sharp right" + + /// The maneuver requires a turn to the right, a merge to the right, or an exit on the right, or the destination is + /// on the right. + case right + + /// The maneuver requires a slight turn to the right. + case slightRight = "slight right" + + /// The maneuver requires no notable change in direction, or the destination is straight ahead. + case straightAhead = "straight" + + /// The maneuver requires a slight turn to the left. + case slightLeft = "slight left" + + /// The maneuver requires a turn to the left, a merge to the left, or an exit on the left, or the destination is on + /// the right. + case left + + /// The maneuver requires a sharp turn to the left. + case sharpLeft = "sharp left" + + /// The maneuver requires a U-turn when possible. + /// + /// Use the difference between the step’s initial and final headings to distinguish between a U-turn to the left + /// (typical in countries that drive on the right) and a U-turn on the right (typical in countries that drive on the + /// left). If the difference in headings is greater than 180 degrees, the maneuver requires a U-turn to the left. If + /// the difference in headings is less than 180 degrees, the maneuver requires a U-turn to the right. + case uTurn = "uturn" + + case undefined + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let rawValue = try? container.decode(String.self) { + self = ManeuverDirection(rawValue: rawValue) ?? .undefined + } else { + self = .undefined + } + } +} + +/// A road sign design standard. +/// +/// A sign standard can affect how a user interface should display information related to the road. For example, a speed +/// limit from the ``RouteLeg/segmentMaximumSpeedLimits`` property may appear in a different-looking view depending on +/// the ``RouteStep/speedLimitSign` property. +public enum SignStandard: String, Codable, Equatable, Sendable { + /// The [Manual on Uniform Traffic Control + /// Devices](https://en.wikipedia.org/wiki/Manual_on_Uniform_Traffic_Control_Devices). + /// + /// This standard has been adopted by the United States and Canada, and several other countries have adopted parts + /// of the standard as well. + case mutcd + + /// The [Vienna Convention on Road Signs and + /// Signals](https://en.wikipedia.org/wiki/Vienna_Convention_on_Road_Signs_and_Signals). + /// + /// This standard is prevalent in Europe and parts of Asia and Latin America. Countries in southern Africa and + /// Central America have adopted similar regional standards. + case viennaConvention = "vienna" +} + +extension String { + func tagValues(separatedBy separator: String) -> [String] { + return components(separatedBy: separator).map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } + } +} + +extension [String] { + func tagValues(joinedBy separator: String) -> String { + return joined(separator: "\(separator) ") + } +} + +/// Encapsulates all the information about a road. +struct Road: Equatable, Sendable { + let names: [String]? + let codes: [String]? + let exitCodes: [String]? + let destinations: [String]? + let destinationCodes: [String]? + let rotaryNames: [String]? + + init( + names: [String]?, + codes: [String]?, + exitCodes: [String]?, + destinations: [String]?, + destinationCodes: [String]?, + rotaryNames: [String]? + ) { + self.names = names + self.codes = codes + self.exitCodes = exitCodes + self.destinations = destinations + self.destinationCodes = destinationCodes + self.rotaryNames = rotaryNames + } + + init(name: String, ref: String?, exits: String?, destination: String?, rotaryName: String?) { + if !name.isEmpty, let ref { + // Directions API v5 profiles powered by Valhalla no longer include the ref in the name. However, the + // `mapbox/cycling` profile, which is powered by OSRM, still includes the ref. + let parenthetical = "(\(ref))" + if name == ref { + self.names = nil + } else { + self.names = name.replacingOccurrences(of: parenthetical, with: "").tagValues(separatedBy: ";") + } + } else { + self.names = name.isEmpty ? nil : name.tagValues(separatedBy: ";") + } + + // Mapbox Directions API v5 combines the destination’s ref and name. + if let destination, destination.contains(": ") { + let destinationComponents = destination.components(separatedBy: ": ") + self.destinationCodes = destinationComponents.first?.tagValues(separatedBy: ",") + self.destinations = destinationComponents.dropFirst().joined(separator: ": ").tagValues(separatedBy: ",") + } else { + self.destinationCodes = nil + self.destinations = destination?.tagValues(separatedBy: ",") + } + + self.exitCodes = exits?.tagValues(separatedBy: ";") + self.codes = ref?.tagValues(separatedBy: ";") + self.rotaryNames = rotaryName?.tagValues(separatedBy: ";") + } +} + +extension Road: Codable { + enum CodingKeys: String, CodingKey, CaseIterable { + case name + case ref + case exits + case destinations + case rotaryName = "rotary_name" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + // Decoder apparently treats an empty string as a null value. + let name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" + let ref = try container.decodeIfPresent(String.self, forKey: .ref) + let exits = try container.decodeIfPresent(String.self, forKey: .exits) + let destinations = try container.decodeIfPresent(String.self, forKey: .destinations) + let rotaryName = try container.decodeIfPresent(String.self, forKey: .rotaryName) + self.init(name: name, ref: ref, exits: exits, destination: destinations, rotaryName: rotaryName) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + let ref = codes?.tagValues(joinedBy: ";") + if var name = names?.tagValues(joinedBy: ";") { + if let ref { + name = "\(name) (\(ref))" + } + try container.encodeIfPresent(name, forKey: .name) + } else { + try container.encode(ref ?? "", forKey: .name) + } + + if var destinations = destinations?.tagValues(joinedBy: ",") { + if let destinationCodes = destinationCodes?.tagValues(joinedBy: ",") { + destinations = "\(destinationCodes): \(destinations)" + } + try container.encode(destinations, forKey: .destinations) + } + + try container.encodeIfPresent(exitCodes?.tagValues(joinedBy: ";"), forKey: .exits) + try container.encodeIfPresent(ref, forKey: .ref) + try container.encodeIfPresent(rotaryNames?.tagValues(joinedBy: ";"), forKey: .rotaryName) + } +} + +/// A ``RouteStep`` object represents a single distinct maneuver along a route and the approach to the next maneuver. +/// The route step object corresponds to a single instruction the user must follow to complete a portion of the route. +/// For example, a step might require the user to turn then follow a road. +/// +/// You do not create instances of this class directly. Instead, you receive route step objects as part of route objects +/// when you request directions using the `Directions.calculate(_:completionHandler:)` method, setting the +/// ``DirectionsOptions/includesSteps`` option to `true` in the ``RouteOptions`` object that you pass into that method. +public struct RouteStep: Codable, ForeignMemberContainer, Equatable, Sendable { + public var foreignMembers: JSONObject = [:] + public var maneuverForeignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey, CaseIterable { + case shape = "geometry" + case distance + case drivingSide = "driving_side" + case expectedTravelTime = "duration" + case typicalTravelTime = "duration_typical" + case instructions + case instructionsDisplayedAlongStep = "bannerInstructions" + case instructionsSpokenAlongStep = "voiceInstructions" + case intersections + case maneuver + case pronunciation + case rotaryPronunciation = "rotary_pronunciation" + case speedLimitSignStandard = "speedLimitSign" + case speedLimitUnit + case transportType = "mode" + } + + private struct Maneuver: Codable, ForeignMemberContainer, Equatable, Sendable { + var foreignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey { + case instruction + case location + case type + case exitIndex = "exit" + case direction = "modifier" + case initialHeading = "bearing_before" + case finalHeading = "bearing_after" + } + + let instructions: String + let maneuverType: ManeuverType + let maneuverDirection: ManeuverDirection? + let maneuverLocation: Turf.LocationCoordinate2D + let initialHeading: Turf.LocationDirection? + let finalHeading: Turf.LocationDirection? + let exitIndex: Int? + + init( + instructions: String, + maneuverType: ManeuverType, + maneuverDirection: ManeuverDirection?, + maneuverLocation: Turf.LocationCoordinate2D, + initialHeading: Turf.LocationDirection?, + finalHeading: Turf.LocationDirection?, + exitIndex: Int? + ) { + self.instructions = instructions + self.maneuverType = maneuverType + self.maneuverLocation = maneuverLocation + self.maneuverDirection = maneuverDirection + self.initialHeading = initialHeading + self.finalHeading = finalHeading + self.exitIndex = exitIndex + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.maneuverLocation = try container.decode(LocationCoordinate2DCodable.self, forKey: .location) + .decodedCoordinates + self.maneuverType = (try? container.decode(ManeuverType.self, forKey: .type)) ?? .default + self.maneuverDirection = try container.decodeIfPresent(ManeuverDirection.self, forKey: .direction) + self.exitIndex = try container.decodeIfPresent(Int.self, forKey: .exitIndex) + + self.initialHeading = try container.decodeIfPresent(Turf.LocationDirection.self, forKey: .initialHeading) + self.finalHeading = try container.decodeIfPresent(Turf.LocationDirection.self, forKey: .finalHeading) + + if let instruction = try? container.decode(String.self, forKey: .instruction) { + self.instructions = instruction + } else { + self.instructions = "\(maneuverType) \(maneuverDirection?.rawValue ?? "")" + } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(instructions, forKey: .instruction) + try container.encode(maneuverType, forKey: .type) + try container.encodeIfPresent(exitIndex, forKey: .exitIndex) + + try container.encodeIfPresent(maneuverDirection, forKey: .direction) + try container.encode(LocationCoordinate2DCodable(maneuverLocation), forKey: .location) + try container.encodeIfPresent(initialHeading, forKey: .initialHeading) + try container.encodeIfPresent(finalHeading, forKey: .finalHeading) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + } + + // MARK: Creating a Step + + /// Initializes a step. + /// - Parameters: + /// - transportType: The mode of transportation used for the step. + /// - maneuverLocation: The location of the maneuver at the beginning of this step. + /// - maneuverType: The type of maneuver required for beginning this step. + /// - maneuverDirection: Additional directional information to clarify the maneuver type. + /// - instructions: A string with instructions explaining how to perform the step’s maneuver. + /// - initialHeading: The user’s heading immediately before performing the maneuver. + /// - finalHeading: The user’s heading immediately after performing the maneuver. + /// - drivingSide: Indicates what side of a bidirectional road the driver must be driving on. Also referred to as + /// the rule of the road. + /// - exitCodes: Any [exit numbers](https://en.wikipedia.org/wiki/Exit_number) assigned to the highway exit at the + /// maneuver. + /// - exitNames: The names of the roundabout exit. + /// - phoneticExitNames: A phonetic or phonemic transcription indicating how to pronounce the names in the + /// ``exitNames`` property. + /// - distance: The step’s distance, measured in meters. + /// - expectedTravelTime: The step's expected travel time, measured in seconds. + /// - typicalTravelTime: The step's typical travel time, measured in seconds. + /// - names: The names of the road or path leading from this step’s maneuver to the next step’s maneuver. + /// - phoneticNames: A phonetic or phonemic transcription indicating how to pronounce the names in the ``names`` + /// property. + /// - codes: Any route reference codes assigned to the road or path leading from this step’s maneuver to the next + /// step’s maneuver. + /// - destinationCodes: Any route reference codes that appear on guide signage for the road leading from this + /// step’s maneuver to the next step’s maneuver. + /// - destinations: Destinations, such as [control cities](https://en.wikipedia.org/wiki/Control_city), that + /// appear on guide signage for the road leading from this step’s maneuver to the next step’s maneuver. + /// - intersections: An array of intersections along the step. + /// - speedLimitSignStandard: The sign design standard used for speed limit signs along the step. + /// - speedLimitUnit: The unit of speed limits on speed limit signs along the step. + /// - instructionsSpokenAlongStep: Instructions about the next step’s maneuver, optimized for speech synthesis. + /// - instructionsDisplayedAlongStep: Instructions about the next step’s maneuver, optimized for display in real + /// time. + /// - administrativeAreaContainerByIntersection: administrative region indices for each ``Intersection`` along the + /// step. + /// - segmentIndicesByIntersection: Segments indices for each ``Intersection`` along the step. + public init( + transportType: TransportType, + maneuverLocation: Turf.LocationCoordinate2D, + maneuverType: ManeuverType, + maneuverDirection: ManeuverDirection? = nil, + instructions: String, + initialHeading: Turf.LocationDirection? = nil, + finalHeading: Turf.LocationDirection? = nil, + drivingSide: DrivingSide, + exitCodes: [String]? = nil, + exitNames: [String]? = nil, + phoneticExitNames: [String]? = nil, + distance: Turf.LocationDistance, + expectedTravelTime: TimeInterval, + typicalTravelTime: TimeInterval? = nil, + names: [String]? = nil, + phoneticNames: [String]? = nil, + codes: [String]? = nil, + destinationCodes: [String]? = nil, + destinations: [String]? = nil, + intersections: [Intersection]? = nil, + speedLimitSignStandard: SignStandard? = nil, + speedLimitUnit: UnitSpeed? = nil, + instructionsSpokenAlongStep: [SpokenInstruction]? = nil, + instructionsDisplayedAlongStep: [VisualInstructionBanner]? = nil, + administrativeAreaContainerByIntersection: [Int?]? = nil, + segmentIndicesByIntersection: [Int?]? = nil + ) { + self.transportType = transportType + self.maneuverLocation = maneuverLocation + self.maneuverType = maneuverType + self.maneuverDirection = maneuverDirection + self.instructions = instructions + self.initialHeading = initialHeading + self.finalHeading = finalHeading + self.drivingSide = drivingSide + self.exitCodes = exitCodes + self.exitNames = exitNames + self.phoneticExitNames = phoneticExitNames + self.distance = distance + self.expectedTravelTime = expectedTravelTime + self.typicalTravelTime = typicalTravelTime + self.names = names + self.phoneticNames = phoneticNames + self.codes = codes + self.destinationCodes = destinationCodes + self.destinations = destinations + self.intersections = intersections + self.speedLimitSignStandard = speedLimitSignStandard + self.speedLimitUnit = speedLimitUnit + self.instructionsSpokenAlongStep = instructionsSpokenAlongStep + self.instructionsDisplayedAlongStep = instructionsDisplayedAlongStep + self.administrativeAreaContainerByIntersection = administrativeAreaContainerByIntersection + self.segmentIndicesByIntersection = segmentIndicesByIntersection + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(instructionsSpokenAlongStep, forKey: .instructionsSpokenAlongStep) + try container.encodeIfPresent(instructionsDisplayedAlongStep, forKey: .instructionsDisplayedAlongStep) + try container.encode(distance, forKey: .distance) + try container.encode(expectedTravelTime, forKey: .expectedTravelTime) + try container.encodeIfPresent(typicalTravelTime, forKey: .typicalTravelTime) + try container.encode(transportType, forKey: .transportType) + + let isRound = maneuverType == .takeRotary || maneuverType == .takeRoundabout + let road = Road( + names: isRound ? exitNames : names, + codes: codes, + exitCodes: exitCodes, + destinations: destinations, + destinationCodes: destinationCodes, + rotaryNames: isRound ? names : nil + ) + try road.encode(to: encoder) + if isRound { + try container.encodeIfPresent(phoneticNames?.tagValues(joinedBy: ";"), forKey: .rotaryPronunciation) + try container.encodeIfPresent(phoneticExitNames?.tagValues(joinedBy: ";"), forKey: .pronunciation) + } else { + try container.encodeIfPresent(phoneticNames?.tagValues(joinedBy: ";"), forKey: .pronunciation) + } + + if let intersectionsToEncode = intersections { + var intersectionsContainer = container.nestedUnkeyedContainer(forKey: .intersections) + try Intersection.encode( + intersections: intersectionsToEncode, + to: &intersectionsContainer, + administrativeRegionIndices: administrativeAreaContainerByIntersection, + segmentIndicesByIntersection: segmentIndicesByIntersection + ) + } + + try container.encode(drivingSide, forKey: .drivingSide) + if let shape { + let options = encoder.userInfo[.options] as? DirectionsOptions + let shapeFormat = options?.shapeFormat ?? .default + let polyLineString = PolyLineString(lineString: shape, shapeFormat: shapeFormat) + try container.encode(polyLineString, forKey: .shape) + } + + var maneuver = Maneuver( + instructions: instructions, + maneuverType: maneuverType, + maneuverDirection: maneuverDirection, + maneuverLocation: maneuverLocation, + initialHeading: initialHeading, + finalHeading: finalHeading, + exitIndex: exitIndex + ) + maneuver.foreignMembers = maneuverForeignMembers + try container.encode(maneuver, forKey: .maneuver) + + try container.encodeIfPresent(speedLimitSignStandard, forKey: .speedLimitSignStandard) + if let speedLimitUnit, + let unit = SpeedLimitDescriptor.UnitDescriptor(unit: speedLimitUnit) + { + try container.encode(unit, forKey: .speedLimitUnit) + } + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + static func decode(from decoder: Decoder, administrativeRegions: [AdministrativeRegion]) throws -> [RouteStep] { + var container = try decoder.unkeyedContainer() + + var steps = [RouteStep]() + while !container.isAtEnd { + let step = try RouteStep(from: container.superDecoder(), administrativeRegions: administrativeRegions) + + steps.append(step) + } + + return steps + } + + /// Used to Decode `Intersection.admin_index` + private struct AdministrativeAreaIndex: Codable, Sendable { + private enum CodingKeys: String, CodingKey { + case administrativeRegionIndex = "admin_index" + } + + var administrativeRegionIndex: Int? + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.administrativeRegionIndex = try container.decodeIfPresent(Int.self, forKey: .administrativeRegionIndex) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(administrativeRegionIndex, forKey: .administrativeRegionIndex) + } + } + + /// Used to Decode `Intersection.geometry_index` + private struct IntersectionShapeIndex: Codable, Sendable { + private enum CodingKeys: String, CodingKey { + case geometryIndex = "geometry_index" + } + + let geometryIndex: Int? + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.geometryIndex = try container.decodeIfPresent(Int.self, forKey: .geometryIndex) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(geometryIndex, forKey: .geometryIndex) + } + } + + public init(from decoder: Decoder) throws { + try self.init(from: decoder, administrativeRegions: nil) + } + + init(from decoder: Decoder, administrativeRegions: [AdministrativeRegion]?) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let maneuver = try container.decode(Maneuver.self, forKey: .maneuver) + + self.maneuverLocation = maneuver.maneuverLocation + self.maneuverType = maneuver.maneuverType + self.maneuverDirection = maneuver.maneuverDirection + self.exitIndex = maneuver.exitIndex + self.initialHeading = maneuver.initialHeading + self.finalHeading = maneuver.finalHeading + self.instructions = maneuver.instructions + self.maneuverForeignMembers = maneuver.foreignMembers + + if let polyLineString = try container.decodeIfPresent(PolyLineString.self, forKey: .shape) { + self.shape = try LineString(polyLineString: polyLineString) + } else { + self.shape = nil + } + + self.drivingSide = try container.decode(DrivingSide.self, forKey: .drivingSide) + + self.instructionsSpokenAlongStep = try container.decodeIfPresent( + [SpokenInstruction].self, + forKey: .instructionsSpokenAlongStep + ) + + if var visuals = try container.decodeIfPresent( + [VisualInstructionBanner].self, + forKey: .instructionsDisplayedAlongStep + ) { + for index in visuals.indices { + visuals[index].drivingSide = drivingSide + } + + self.instructionsDisplayedAlongStep = visuals + } else { + self.instructionsDisplayedAlongStep = nil + } + + self.distance = try container.decode(Turf.LocationDirection.self, forKey: .distance) + self.expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime) + self.typicalTravelTime = try container.decodeIfPresent(TimeInterval.self, forKey: .typicalTravelTime) + + self.transportType = try container.decode(TransportType.self, forKey: .transportType) + self.administrativeAreaContainerByIntersection = try container.decodeIfPresent( + [AdministrativeAreaIndex].self, + forKey: .intersections + )? + .map(\.administrativeRegionIndex) + var rawIntersections = try container.decodeIfPresent([Intersection].self, forKey: .intersections) + + // Updating `Intersection.regionCode` since we removed it's `admin_index` for convenience + if let administrativeRegions, + rawIntersections != nil, + let rawAdminIndicies = administrativeAreaContainerByIntersection + { + for index in 0.. regionIndex + { + rawIntersections![index].updateRegionCode(administrativeRegions[regionIndex].countryCode) + } + } + } + + self.intersections = rawIntersections + + self.segmentIndicesByIntersection = try container.decodeIfPresent( + [IntersectionShapeIndex].self, + forKey: .intersections + )?.map(\.geometryIndex) + + let road = try Road(from: decoder) + self.codes = road.codes + self.exitCodes = road.exitCodes + self.destinations = road.destinations + self.destinationCodes = road.destinationCodes + + self.speedLimitSignStandard = try container.decodeIfPresent(SignStandard.self, forKey: .speedLimitSignStandard) + self.speedLimitUnit = try (container.decodeIfPresent( + SpeedLimitDescriptor.UnitDescriptor.self, + forKey: .speedLimitUnit + ))?.describedUnit + + let type = maneuverType + if type == .takeRotary || type == .takeRoundabout { + self.names = road.rotaryNames + self.phoneticNames = try container.decodeIfPresent(String.self, forKey: .rotaryPronunciation)? + .tagValues(separatedBy: ";") + self.exitNames = road.names + self.phoneticExitNames = try container.decodeIfPresent(String.self, forKey: .pronunciation)? + .tagValues(separatedBy: ";") + } else { + self.names = road.names + self.phoneticNames = try container.decodeIfPresent(String.self, forKey: .pronunciation)? + .tagValues(separatedBy: ";") + self.exitNames = nil + self.phoneticExitNames = nil + } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + try decodeForeignMembers(notKeyedBy: Road.CodingKeys.self, with: decoder) + } + + // MARK: Getting the Shape of the Step + + /// The path of the route step from the location of the maneuver to the location of the next step’s maneuver. + /// + /// The value of this property may be `nil`, for example when the maneuver type is ``ManeuverType/arrive``. + /// + /// Using the [Mapbox Maps SDK for iOS](https://www.mapbox.com/ios-sdk/) or [Mapbox Maps SDK for + /// macOS](https://github.com/mapbox/mapbox-gl-native/tree/master/platform/macos/), you can create an `MGLPolyline` + /// object using the `LineString.coordinates` property to display a portion of a route on an `MGLMapView`. + public var shape: LineString? + + // MARK: Getting the Mode of Transportation + + /// The mode of transportation used for the step. + /// + /// This step may use a different mode of transportation than the overall route. + public let transportType: TransportType + + // MARK: Getting Details About the Maneuver + + /// The location of the maneuver at the beginning of this step. + public let maneuverLocation: Turf.LocationCoordinate2D + + /// The type of maneuver required for beginning this step. + public let maneuverType: ManeuverType + + /// Additional directional information to clarify the maneuver type. + public let maneuverDirection: ManeuverDirection? + + /// A string with instructions explaining how to perform the step’s maneuver. + /// + /// You can display this string or read it aloud to the user. The string does not include the distance to or from + /// the maneuver. For instructions optimized for real-time delivery during turn-by-turn navigation, set the + /// ``DirectionsOptions/includesSpokenInstructions`` option and use the ``instructionsSpokenAlongStep`` property. If + /// you need customized instructions, you can construct them yourself from the step’s other properties or use [OSRM + /// Text Instructions](https://github.com/Project-OSRM/osrm-text-instructions.swift/). + /// + /// - Note: If you use the MapboxDirections framework with the Mapbox Directions API, this property is formatted and + /// localized for display to the user. If you use OSRM directly, this property contains a basic string that only + /// includes the maneuver type and direction. Use [OSRM Text + /// Instructions](https://github.com/Project-OSRM/osrm-text-instructions.swift/) to construct a complete, localized + /// instruction string for display. + public let instructions: String + + /// The user’s heading immediately before performing the maneuver. + public let initialHeading: Turf.LocationDirection? + + /// The user’s heading immediately after performing the maneuver. + /// + /// The value of this property may differ from the user’s heading after traveling along the road past the maneuver. + public let finalHeading: Turf.LocationDirection? + + /// Indicates what side of a bidirectional road the driver must be driving on. Also referred to as the rule of the + /// road. + public let drivingSide: DrivingSide + + /// The number of exits from the previous maneuver up to and including this step’s maneuver. + /// + /// If the maneuver takes place on a surface street, this property counts intersections. The number of intersections + /// does not necessarily correspond to the number of blocks. If the maneuver takes place on a grade-separated + /// highway (freeway or motorway), this property counts highway exits but not highway entrances. If the maneuver is + /// a roundabout maneuver, the exit index is the number of exits from the approach to the recommended outlet. For + /// the signposted exit numbers associated with a highway exit, use the ``exitCodes`` property. + /// + /// In some cases, the number of exits leading to a maneuver may be more useful to the user than the distance to the + /// maneuver. + public var exitIndex: Int? + + /// Any [exit numbers](https://en.wikipedia.org/wiki/Exit_number) assigned to the highway exit at the maneuver. + /// + /// This property is only set when the ``maneuverType`` is ``ManeuverType/takeOffRamp``. For the number of exits + /// from the previous maneuver, regardless of the highway’s exit numbering scheme, use the ``exitIndex`` property. + /// For the route reference codes associated with the connecting road, use the ``destinationCodes`` property. For + /// the names associated with a roundabout exit, use the ``exitNames`` property. + /// + /// An exit number is an alphanumeric identifier posted at or ahead of a highway off-ramp. Exit numbers may increase + /// or decrease sequentially along a road, or they may correspond to distances from either end of the road. An + /// alphabetic suffix may appear when multiple exits are located in the same interchange. If multiple exits are + /// [combined into a single + /// exit](https://en.wikipedia.org/wiki/Local-express_lanes#Example_of_cloverleaf_interchanges), the step may have + /// multiple exit codes. + public let exitCodes: [String]? + + /// The names of the roundabout exit. + /// + /// This property is only set for roundabout (traffic circle or rotary) maneuvers. For the signposted names + /// associated with a highway exit, use the ``destinations`` property. For the signposted exit numbers, use the + /// ``exitCodes`` property. + /// + /// If you display a name to the user, you may need to abbreviate common words like “East” or “Boulevard” to ensure + /// that it fits in the allotted space. + public let exitNames: [String]? + + /// A phonetic or phonemic transcription indicating how to pronounce the names in the ``exitNames`` property. + /// + /// This property is only set for roundabout (traffic circle or rotary) maneuvers. + /// + /// The transcription is written in the [International Phonetic + /// Alphabet](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet). + public let phoneticExitNames: [String]? + + // MARK: Getting Details About the Approach to the Next Maneuver + + /// The step’s distance, measured in meters. + /// + /// The value of this property accounts for the distance that the user must travel to go from this step’s maneuver + /// location to the next step’s maneuver location. It is not the sum of the direct distances between the route’s + /// waypoints, nor should you assume that the user would travel along this distance at a fixed speed. + public let distance: Turf.LocationDistance + + /// The step’s expected travel time, measured in seconds. + /// + /// The value of this property reflects the time it takes to go from this step’s maneuver location to the next + /// step’s maneuver location. If the route was calculated using the ``ProfileIdentifier/automobileAvoidingTraffic`` + /// profile, this property reflects current traffic conditions at the time of the request, not necessarily the + /// traffic conditions at the time the user would begin this step. For other profiles, this property reflects travel + /// time under ideal conditions and does not account for traffic congestion. If the step makes use of a ferry or + /// train, the actual travel time may additionally be subject to the schedules of those services. + /// + /// Do not assume that the user would travel along the step at a fixed speed. For the expected travel time on each + /// individual segment along the leg, specify the ``AttributeOptions/expectedTravelTime`` option and use the + /// ``RouteLeg/expectedSegmentTravelTimes`` property. + public var expectedTravelTime: TimeInterval + + /// The step’s typical travel time, measured in seconds. + /// + /// The value of this property reflects the typical time it takes to go from this step’s maneuver location to the + /// next step’s maneuver location. This property is available when using the + /// ``ProfileIdentifier/automobileAvoidingTraffic`` profile. This property reflects typical traffic conditions at + /// the time of the request, not necessarily the typical traffic conditions at the time the user would begin this + /// step. If the step makes use of a ferry, the typical travel time may additionally be subject to the schedule of + /// this service. + /// + /// Do not assume that the user would travel along the step at a fixed speed. + public var typicalTravelTime: TimeInterval? + + /// The names of the road or path leading from this step’s maneuver to the next step’s maneuver. + /// + /// If the maneuver is a turning maneuver, the step’s names are the name of the road or path onto which the user + /// turns. If you display a name to the user, you may need to abbreviate common words like “East” or “Boulevard” to + /// ensure that it fits in the allotted space. + /// + /// If the maneuver is a roundabout maneuver, the outlet to take is named in the ``exitNames`` property; the + /// ``names`` property is only set for large roundabouts that have their own names. + public let names: [String]? + + /// A phonetic or phonemic transcription indicating how to pronounce the names in the `names` property. + /// + /// The transcription is written in the [International Phonetic + /// Alphabet](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet). + /// + /// If the maneuver traverses a large, named roundabout, this property contains a hint about how to pronounce the + /// names of the outlet to take. + public let phoneticNames: [String]? + + /// Any route reference codes assigned to the road or path leading from this step’s maneuver to the next step’s + /// maneuver. + /// + /// A route reference code commonly consists of an alphabetic network code, a space or hyphen, and a route number. + /// You should not assume that the network code is globally unique: for example, a network code of “NH” may indicate + /// a “National Highway” or “New Hampshire”. Moreover, a route number may not even uniquely identify a route within + /// a given network. + /// + /// If a highway ramp is part of a numbered route, its reference code is contained in this property. On the other + /// hand, guide signage for a highway ramp usually indicates route reference codes of the adjoining road; use the + /// ``destinationCodes`` property for those route reference codes. + public let codes: [String]? + + /// Any route reference codes that appear on guide signage for the road leading from this step’s maneuver to the + /// next step’s maneuver. + /// + /// This property is typically available in steps leading to or from a freeway or expressway. This property contains + /// route reference codes associated with a road later in the route. If a highway ramp is itself part of a numbered + /// route, its reference code is contained in the `codes` property. For the signposted exit numbers associated with + /// a highway exit, use the `exitCodes` property. + /// + /// A route reference code commonly consists of an alphabetic network code, a space or hyphen, and a route number. + /// You should not assume that the network code is globally unique: for example, a network code of “NH” may indicate + /// a “National Highway” or “New Hampshire”. Moreover, a route number may not even uniquely identify a route within + /// a given network. A destination code for a divided road is often suffixed with the cardinal direction of travel, + /// for example “I 80 East”. + public let destinationCodes: [String]? + + /// Destinations, such as [control cities](https://en.wikipedia.org/wiki/Control_city), that appear on guide signage + /// for the road leading from this step’s maneuver to the next step’s maneuver. + /// + /// This property is typically available in steps leading to or from a freeway or expressway. + public let destinations: [String]? + + /// An array of intersections along the step. + /// + /// Each item in the array corresponds to a cross street, starting with the intersection at the maneuver location + /// indicated by the coordinates property and continuing with each cross street along the step. + public let intersections: [Intersection]? + + /// Each intersection’s administrative region index. + /// + /// This property is set to `nil` if the ``intersections`` property is `nil`. An individual array element may be + /// `nil` if the corresponding ``Intersection`` instance has no administrative region assigned. + /// - SeeAlso: ``Intersection/regionCode``, ``RouteLeg/regionCode(atStepIndex:intersectionIndex:)`` + public let administrativeAreaContainerByIntersection: [Int?]? + + /// Segments indices for each ``Intersection`` along the step. + /// + /// The indices are arranged in the same order as the items of ``intersections``. This property is `nil` if + /// ``intersections`` is `nil`. An individual item may be `nil` if the corresponding JSON-formatted intersection + /// object has no `geometry_index` property. + public let segmentIndicesByIntersection: [Int?]? + + /// The sign design standard used for speed limit signs along the step. + /// + /// This standard affects how corresponding speed limits in the ``RouteLeg/segmentMaximumSpeedLimits`` property + /// should be displayed. + public let speedLimitSignStandard: SignStandard? + + /// The unit of speed limits on speed limit signs along the step. + /// + /// This standard affects how corresponding speed limits in the ``RouteLeg/segmentMaximumSpeedLimits`` property + /// should be displayed. + public let speedLimitUnit: UnitSpeed? + + // MARK: Getting Details About the Next Maneuver + + /// Instructions about the next step’s maneuver, optimized for speech synthesis. + /// + /// As the user traverses this step, you can give them advance notice of the upcoming maneuver by reading aloud each + /// item in this array in order as the user reaches the specified distances along this step. The text of the spoken + /// instructions refers to the details in the next step, but the distances are measured from the beginning of this + /// step. + /// + /// This property is non-`nil` if the ``DirectionsOptions/includesSpokenInstructions`` option is set to `true`. For + /// instructions designed for display, use the ``instructions`` property. + public let instructionsSpokenAlongStep: [SpokenInstruction]? + + /// Instructions about the next step’s maneuver, optimized for display in real time. + /// + /// As the user traverses this step, you can give them advance notice of the upcoming maneuver by displaying each + /// item in this array in order as the user reaches the specified distances along this step. The text and images of + /// the visual instructions refer to the details in the next step, but the distances are measured from the beginning + /// of this step. + /// + /// This property is non-`nil` if the ``DirectionsOptions/includesVisualInstructions`` option is set to `true`. For + /// instructions designed for speech synthesis, use the ``instructionsSpokenAlongStep`` property. For instructions + /// designed for display in a static list, use the ``instructions`` property. + public let instructionsDisplayedAlongStep: [VisualInstructionBanner]? +} + +extension RouteStep: CustomStringConvertible { + public var description: String { + return instructions + } +} + +extension RouteStep: CustomQuickLookConvertible { + func debugQuickLookObject() -> Any? { + guard let shape else { + return nil + } + return debugQuickLookURL(illustrating: shape) + } +} + +extension RouteStep { + public static func == (lhs: RouteStep, rhs: RouteStep) -> Bool { + // Compare all the properties, from cheapest to most expensive to compare. + return lhs.initialHeading == rhs.initialHeading && + lhs.finalHeading == rhs.finalHeading && + lhs.instructions == rhs.instructions && + lhs.exitIndex == rhs.exitIndex && + lhs.distance == rhs.distance && + lhs.expectedTravelTime == rhs.expectedTravelTime && + lhs.typicalTravelTime == rhs.typicalTravelTime && + + lhs.maneuverType == rhs.maneuverType && + lhs.maneuverDirection == rhs.maneuverDirection && + lhs.drivingSide == rhs.drivingSide && + lhs.transportType == rhs.transportType && + + lhs.maneuverLocation == rhs.maneuverLocation && + + lhs.exitCodes == rhs.exitCodes && + lhs.exitNames == rhs.exitNames && + lhs.phoneticExitNames == rhs.phoneticExitNames && + lhs.names == rhs.names && + lhs.phoneticNames == rhs.phoneticNames && + lhs.codes == rhs.codes && + lhs.destinationCodes == rhs.destinationCodes && + lhs.destinations == rhs.destinations && + + lhs.speedLimitSignStandard == rhs.speedLimitSignStandard && + lhs.speedLimitUnit == rhs.speedLimitUnit && + + lhs.intersections == rhs.intersections && + lhs.instructionsSpokenAlongStep == rhs.instructionsSpokenAlongStep && + lhs.instructionsDisplayedAlongStep == rhs.instructionsDisplayedAlongStep && + + lhs.shape == rhs.shape + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/SilentWaypoint.swift b/ios/Classes/Navigation/MapboxDirections/SilentWaypoint.swift new file mode 100644 index 000000000..bbd9b4955 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/SilentWaypoint.swift @@ -0,0 +1,48 @@ +import Foundation +import Turf + +/// Represents a silent waypoint along the ``RouteLeg``. +/// +/// See ``RouteLeg/viaWaypoints`` for more details. +public struct SilentWaypoint: Codable, Equatable, ForeignMemberContainer, Sendable { + public var foreignMembers: JSONObject = [:] + + public enum CodingKeys: String, CodingKey { + case waypointIndex = "waypoint_index" + case distanceFromStart = "distance_from_start" + case shapeCoordinateIndex = "geometry_index" + } + + /// The associated waypoint index in `RouteResponse.waypoints`, excluding the origin (index 0) and destination. + public var waypointIndex: Int + + /// The calculated distance, in meters, from the leg origin. + public var distanceFromStart: Double + + /// The associated ``Route`` shape index of the silent waypoint location. + public var shapeCoordinateIndex: Int + + public init(waypointIndex: Int, distanceFromStart: Double, shapeCoordinateIndex: Int) { + self.waypointIndex = waypointIndex + self.distanceFromStart = distanceFromStart + self.shapeCoordinateIndex = shapeCoordinateIndex + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.waypointIndex = try container.decode(Int.self, forKey: .waypointIndex) + self.distanceFromStart = try container.decode(Double.self, forKey: .distanceFromStart) + self.shapeCoordinateIndex = try container.decode(Int.self, forKey: .shapeCoordinateIndex) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(waypointIndex, forKey: .waypointIndex) + try container.encode(distanceFromStart, forKey: .distanceFromStart) + try container.encode(shapeCoordinateIndex, forKey: .shapeCoordinateIndex) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/SpokenInstruction.swift b/ios/Classes/Navigation/MapboxDirections/SpokenInstruction.swift new file mode 100644 index 000000000..fdf813784 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/SpokenInstruction.swift @@ -0,0 +1,83 @@ +import Foundation +import Turf + +/// An instruction about an upcoming ``RouteStep``’s maneuver, optimized for speech synthesis. +/// +/// The instruction is provided in two formats: plain text and text marked up according to the [Speech Synthesis Markup +/// Language](https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language) (SSML). Use a speech synthesizer such as +/// `AVSpeechSynthesizer` or Amazon Polly to read aloud the instruction. +/// +/// The ``SpokenInstruction/distanceAlongStep`` property is measured from the beginning of the step associated with this +/// object. By contrast, the `text` and `ssmlText` properties refer to the details in the following step. It is also +/// possible for the instruction to refer to two following steps simultaneously when needed for safe navigation. +public struct SpokenInstruction: Codable, ForeignMemberContainer, Equatable, Sendable { + public var foreignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey, CaseIterable { + case distanceAlongStep = "distanceAlongGeometry" + case text = "announcement" + case ssmlText = "ssmlAnnouncement" + } + + // MARK: Creating a Spoken Instruction + + /// Initialize a spoken instruction. + /// - Parameters: + /// - distanceAlongStep: A distance along the associated ``RouteStep`` at which to read the instruction aloud. + /// - text: A plain-text representation of the speech-optimized instruction. + /// - ssmlText: A formatted representation of the speech-optimized instruction. + public init(distanceAlongStep: LocationDistance, text: String, ssmlText: String) { + self.distanceAlongStep = distanceAlongStep + self.text = text + self.ssmlText = ssmlText + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.distanceAlongStep = try container.decode(LocationDistance.self, forKey: .distanceAlongStep) + self.text = try container.decode(String.self, forKey: .text) + self.ssmlText = try container.decode(String.self, forKey: .ssmlText) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(distanceAlongStep, forKey: .distanceAlongStep) + try container.encode(text, forKey: .text) + try container.encode(ssmlText, forKey: .ssmlText) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + // MARK: Timing When to Say the Instruction + + /// A distance along the associated ``RouteStep`` at which to read the instruction aloud. + /// + /// The distance is measured in meters from the beginning of the associated step. + public let distanceAlongStep: LocationDistance + + // MARK: Getting the Instruction to Say + + /// A plain-text representation of the speech-optimized instruction. + /// This representation is appropriate for speech synthesizers that lack support for the [Speech Synthesis Markup + /// Language](https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language) (SSML), such as `AVSpeechSynthesizer`. + /// For speech synthesizers that support SSML, use the ``ssmlText`` property instead. + public let text: String + + /// A formatted representation of the speech-optimized instruction. + /// + /// This representation is appropriate for speech synthesizers that support the [Speech Synthesis Markup + /// Language](https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language) (SSML), such as [Amazon + /// Polly](https://aws.amazon.com/polly/). Numbers and names are marked up to ensure correct pronunciation. For + /// speech synthesizers that lack SSML support, use the ``text`` property instead. + public let ssmlText: String +} + +extension SpokenInstruction { + public static func == (lhs: SpokenInstruction, rhs: SpokenInstruction) -> Bool { + return lhs.distanceAlongStep == rhs.distanceAlongStep && + lhs.text == rhs.text && + lhs.ssmlText == rhs.ssmlText + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/TollCollection.swift b/ios/Classes/Navigation/MapboxDirections/TollCollection.swift new file mode 100644 index 000000000..953504a67 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/TollCollection.swift @@ -0,0 +1,52 @@ +import Foundation +import Turf + +/// `TollCollection` describes corresponding object on the route. +public struct TollCollection: Codable, Equatable, ForeignMemberContainer, Sendable { + public var foreignMembers: JSONObject = [:] + + public enum CollectionType: String, Codable, Sendable { + case booth = "toll_booth" + case gantry = "toll_gantry" + } + + /// The type of the toll collection point. + public let type: CollectionType + + /// The name of the toll collection point. + public var name: String? + + private enum CodingKeys: String, CodingKey { + case type + case name + } + + public init(type: CollectionType) { + self.init(type: type, name: nil) + } + + public init(type: CollectionType, name: String?) { + self.type = type + self.name = name + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.type = try container.decode(CollectionType.self, forKey: .type) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + try container.encodeIfPresent(name, forKey: .name) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.type == rhs.type + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/TollPrice.swift b/ios/Classes/Navigation/MapboxDirections/TollPrice.swift new file mode 100644 index 000000000..d4466904a --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/TollPrice.swift @@ -0,0 +1,147 @@ +import Foundation +import Turf + +/// Information about toll payment method. +public struct TollPaymentMethod: Hashable, Equatable, Sendable { + /// Method identifier. + public let identifier: String + + /// Payment is done by electronic toll collection. + public static let electronicTollCollection = TollPaymentMethod(identifier: "etc") + /// Payment is done by cash. + public static let cash = TollPaymentMethod(identifier: "cash") +} + +/// Categories by which toll fees are divided. +public struct TollCategory: Hashable, Equatable, Sendable { + /// Category name. + public let name: String + + /// A small sized vehicle. + /// + /// In Japan, this is a [standard vehicle size](https://en.wikipedia.org/wiki/Expressways_of_Japan#Tolls). + public static let small = TollCategory(name: "small") + /// A standard sized vehicle. + /// + /// In Japan, this is a [standard vehicle size](https://en.wikipedia.org/wiki/Expressways_of_Japan#Tolls). + public static let standard = TollCategory(name: "standard") + /// A middle sized vehicle. + /// + /// In Japan, this is a [standard vehicle size](https://en.wikipedia.org/wiki/Expressways_of_Japan#Tolls). + public static let middle = TollCategory(name: "middle") + /// A large sized vehicle. + /// + /// In Japan, this is a [standard vehicle size](https://en.wikipedia.org/wiki/Expressways_of_Japan#Tolls). + public static let large = TollCategory(name: "large") + /// A jumbo sized vehicle. + /// + /// In Japan, this is a [standard vehicle size](https://en.wikipedia.org/wiki/Expressways_of_Japan#Tolls). + public static let jumbo = TollCategory(name: "jumbo") +} + +/// Toll cost information for the ``Route``. +public struct TollPrice: Equatable, Hashable, ForeignMemberContainer, Sendable { + public var foreignMembers: Turf.JSONObject = [:] + + /// Related currency code string. + /// + /// Uses ISO 4217 format. Refers to ``amount`` value. + /// This value is compatible with `NumberFormatter().currencyCode`. + public let currencyCode: String + /// Information about toll payment. + public let paymentMethod: TollPaymentMethod + /// Toll category information. + public let category: TollCategory + /// The actual toll price in ``currencyCode`` currency. + /// + /// A toll cost of `0` is valid and simply means that no toll costs are incurred for this route. + public let amount: Decimal + + init(currencyCode: String, paymentMethod: TollPaymentMethod, category: TollCategory, amount: Decimal) { + self.currencyCode = currencyCode + self.paymentMethod = paymentMethod + self.category = category + self.amount = amount + } +} + +struct TollPriceCoder: Codable, Sendable { + let tollPrices: [TollPrice] + + init(tollPrices: [TollPrice]) { + self.tollPrices = tollPrices + } + + private class TollPriceItem: Codable, ForeignMemberContainerClass { + var foreignMembers: Turf.JSONObject = [:] + + private enum CodingKeys: String, CodingKey, CaseIterable { + case currency + case paymentMethods = "payment_methods" + } + + var currencyCode: String + var paymentMethods: [String: [String: Decimal]] = [:] + + init(currencyCode: String) { + self.currencyCode = currencyCode + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.currencyCode = try container.decode(String.self, forKey: .currency) + self.paymentMethods = try container.decode([String: [String: Decimal]].self, forKey: .paymentMethods) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(currencyCode, forKey: .currency) + try container.encode(paymentMethods, forKey: .paymentMethods) + + try encodeForeignMembers(to: encoder) + } + } + + init(from decoder: Decoder) throws { + let item = try TollPriceItem(from: decoder) + + var tollPrices = [TollPrice]() + for method in item.paymentMethods { + for category in method.value { + var newItem = TollPrice( + currencyCode: item.currencyCode, + paymentMethod: TollPaymentMethod(identifier: method.key), + category: TollCategory(name: category.key), + amount: category.value + ) + newItem.foreignMembers = item.foreignMembers + tollPrices.append(newItem) + } + } + self.tollPrices = tollPrices + } + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + var items: [TollPriceItem] = [] + + for price in tollPrices { + var item: TollPriceItem + if let existingItem = items.first(where: { $0.currencyCode == price.currencyCode }) { + item = existingItem + } else { + item = TollPriceItem(currencyCode: price.currencyCode) + item.foreignMembers = price.foreignMembers + items.append(item) + } + if item.paymentMethods[price.paymentMethod.identifier] == nil { + item.paymentMethods[price.paymentMethod.identifier] = [:] + } + item.paymentMethods[price.paymentMethod.identifier]?[price.category.name] = price.amount + } + + try container.encode(contentsOf: items) + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/TrafficTendency.swift b/ios/Classes/Navigation/MapboxDirections/TrafficTendency.swift new file mode 100644 index 000000000..d0fa7f77e --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/TrafficTendency.swift @@ -0,0 +1,20 @@ +import Foundation + +/// :nodoc: +/// The tendency value conveys the changing state of traffic congestion (increasing, decreasing, constant etc). +/// +/// New values could be introduced in the future without an API version change. +public enum TrafficTendency: Int, Codable, CaseIterable, Equatable, Sendable { + /// Congestion tendency is unknown. + case unknown = 0 + /// Congestion tendency is not changing. + case constant = 1 + /// Congestion tendency is increasing. + case increasing = 2 + /// Congestion tendency is decreasing. + case decreasing = 3 + /// Congestion tendency is rapidly increasing. + case rapidlyIncreasing = 4 + /// Congestion tendency is rapidly decreasing. + case rapidlyDecreasing = 5 +} diff --git a/ios/Classes/Navigation/MapboxDirections/VisualInstruction.swift b/ios/Classes/Navigation/MapboxDirections/VisualInstruction.swift new file mode 100644 index 000000000..b915c0e11 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/VisualInstruction.swift @@ -0,0 +1,95 @@ +import Foundation +import Turf + +/// The contents of a banner that should be displayed as added visual guidance for a route. The banner instructions are +/// children of the steps during which they should be displayed, but they refer to the maneuver in the following step. +public struct VisualInstruction: Codable, ForeignMemberContainer, Equatable, Sendable { + public var foreignMembers: JSONObject = [:] + + // MARK: Creating a Visual Instruction + + private enum CodingKeys: String, CodingKey, CaseIterable { + case text + case maneuverType = "type" + case maneuverDirection = "modifier" + case components + case finalHeading = "degrees" + } + + /// Initializes a new visual instruction banner object that displays the given information. + public init( + text: String?, + maneuverType: ManeuverType?, + maneuverDirection: ManeuverDirection?, + components: [Component], + degrees: LocationDegrees? = nil + ) { + self.text = text + self.maneuverType = maneuverType + self.maneuverDirection = maneuverDirection + self.components = components + self.finalHeading = degrees + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(text, forKey: .text) + try container.encodeIfPresent(maneuverType, forKey: .maneuverType) + try container.encodeIfPresent(maneuverDirection, forKey: .maneuverDirection) + try container.encode(components, forKey: .components) + try container.encodeIfPresent(finalHeading, forKey: .finalHeading) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.text = try container.decodeIfPresent(String.self, forKey: .text) + self.maneuverType = try container.decodeIfPresent(ManeuverType.self, forKey: .maneuverType) + self.maneuverDirection = try container.decodeIfPresent(ManeuverDirection.self, forKey: .maneuverDirection) + self.components = try container.decode([Component].self, forKey: .components) + self.finalHeading = try container.decodeIfPresent(LocationDegrees.self, forKey: .finalHeading) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + // MARK: Displaying the Instruction Text + + /// A plain text representation of the instruction. + /// + /// This property is set to `nil` when the ``text`` property in the Mapbox Directions API response is an empty + /// string. + public let text: String? + + /// A structured representation of the instruction. + public let components: [Component] + + // MARK: Displaying a Maneuver Image + + /// The type of maneuver required for beginning the step described by the visual instruction. + public var maneuverType: ManeuverType? + + /// Additional directional information to clarify the maneuver type. + public var maneuverDirection: ManeuverDirection? + + /// The heading at which the user exits a roundabout (traffic circle or rotary). + /// + /// This property is measured in degrees clockwise relative to the user’s initial heading. A value of 180° means + /// continuing through the roundabout without changing course, whereas a value of 0° means traversing the entire + /// roundabout back to the entry point. + /// + /// This property is only relevant if the ``maneuverType`` is any of the following values: + /// ``ManeuverType/takeRoundabout``, ``ManeuverType/takeRotary``, ``ManeuverType/turnAtRoundabout``, + /// ``ManeuverType/exitRoundabout``, or ``ManeuverType/exitRotary``. + public var finalHeading: LocationDegrees? +} + +extension VisualInstruction { + public static func == (lhs: VisualInstruction, rhs: VisualInstruction) -> Bool { + return lhs.text == rhs.text && + lhs.maneuverType == rhs.maneuverType && + lhs.maneuverDirection == rhs.maneuverDirection && + lhs.components == rhs.components && + lhs.finalHeading == rhs.finalHeading + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/VisualInstructionBanner.swift b/ios/Classes/Navigation/MapboxDirections/VisualInstructionBanner.swift new file mode 100644 index 000000000..1ddc572f7 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/VisualInstructionBanner.swift @@ -0,0 +1,112 @@ +import Foundation +import Turf + +extension CodingUserInfoKey { + static let drivingSide = CodingUserInfoKey(rawValue: "drivingSide")! +} + +/// A visual instruction banner contains all the information necessary for creating a visual cue about a given +/// ``RouteStep``. +public struct VisualInstructionBanner: Codable, ForeignMemberContainer, Equatable, Sendable { + public var foreignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey, CaseIterable { + case distanceAlongStep = "distanceAlongGeometry" + case primaryInstruction = "primary" + case secondaryInstruction = "secondary" + case tertiaryInstruction = "sub" + case quaternaryInstruction = "view" + case drivingSide + } + + // MARK: Creating a Visual Instruction Banner + + /// Initializes a visual instruction banner with the given instructions. + public init( + distanceAlongStep: LocationDistance, + primary: VisualInstruction, + secondary: VisualInstruction?, + tertiary: VisualInstruction?, + quaternary: VisualInstruction?, + drivingSide: DrivingSide + ) { + self.distanceAlongStep = distanceAlongStep + self.primaryInstruction = primary + self.secondaryInstruction = secondary + self.tertiaryInstruction = tertiary + self.quaternaryInstruction = quaternary + self.drivingSide = drivingSide + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(distanceAlongStep, forKey: .distanceAlongStep) + try container.encode(primaryInstruction, forKey: .primaryInstruction) + try container.encodeIfPresent(secondaryInstruction, forKey: .secondaryInstruction) + try container.encodeIfPresent(tertiaryInstruction, forKey: .tertiaryInstruction) + try container.encodeIfPresent(quaternaryInstruction, forKey: .quaternaryInstruction) + try container.encode(drivingSide, forKey: .drivingSide) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.distanceAlongStep = try container.decode(LocationDistance.self, forKey: .distanceAlongStep) + self.primaryInstruction = try container.decode(VisualInstruction.self, forKey: .primaryInstruction) + self.secondaryInstruction = try container.decodeIfPresent(VisualInstruction.self, forKey: .secondaryInstruction) + self.tertiaryInstruction = try container.decodeIfPresent(VisualInstruction.self, forKey: .tertiaryInstruction) + self.quaternaryInstruction = try container.decodeIfPresent( + VisualInstruction.self, + forKey: .quaternaryInstruction + ) + if let directlyEncoded = try container.decodeIfPresent(DrivingSide.self, forKey: .drivingSide) { + self.drivingSide = directlyEncoded + } else { + self.drivingSide = .default + } + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + // MARK: Timing When to Display the Banner + + /// The distance at which the visual instruction should be shown, measured in meters from the beginning of the step. + public let distanceAlongStep: LocationDistance + + // MARK: Getting the Instructions to Display + + /// The most important information to convey to the user about the ``RouteStep``. + public let primaryInstruction: VisualInstruction + + /// Less important details about the ``RouteStep``. + public let secondaryInstruction: VisualInstruction? + + /// A visual instruction that is presented simultaneously to provide information about an additional maneuver that + /// occurs in rapid succession. + /// + /// This instruction could either contain the visual layout information or the lane information about the upcoming + /// maneuver. + public let tertiaryInstruction: VisualInstruction? + + /// A visual instruction that is presented to provide information about the incoming junction. + /// + /// This instruction displays a zoomed image of incoming junction. + public let quaternaryInstruction: VisualInstruction? + + // MARK: Respecting Regional Driving Rules + + /// Which side of a bidirectional road the driver should drive on, also known as the rule of the road. + public var drivingSide: DrivingSide +} + +extension VisualInstructionBanner { + public static func == (lhs: VisualInstructionBanner, rhs: VisualInstructionBanner) -> Bool { + return lhs.distanceAlongStep == rhs.distanceAlongStep && + lhs.primaryInstruction == rhs.primaryInstruction && + lhs.secondaryInstruction == rhs.secondaryInstruction && + lhs.tertiaryInstruction == rhs.tertiaryInstruction && + lhs.quaternaryInstruction == rhs.quaternaryInstruction && + lhs.drivingSide == rhs.drivingSide + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/VisualInstructionComponent.swift b/ios/Classes/Navigation/MapboxDirections/VisualInstructionComponent.swift new file mode 100644 index 000000000..64040f651 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/VisualInstructionComponent.swift @@ -0,0 +1,345 @@ +import Foundation + +#if canImport(CoreGraphics) +import CoreGraphics +#if os(macOS) +import Cocoa +#elseif os(watchOS) +import WatchKit +#else +import UIKit +#endif +#endif + +#if canImport(CoreGraphics) +/// An image scale factor. +public typealias Scale = CGFloat +#else +/// An image scale factor. +public typealias Scale = Double +#endif + +extension VisualInstruction { + /// A unit of information displayed to the user as part of a ``VisualInstruction``. + public enum Component: Equatable, Sendable { + /// The component separates two other destination components. + /// + /// If the two adjacent components are both displayed as images, you can hide this delimiter component. + case delimiter(text: TextRepresentation) + + /// The component bears the name of a place or street. + case text(text: TextRepresentation) + + /// The component is an image, such as a [route marker](https://en.wikipedia.org/wiki/Highway_shield), with a + /// fallback text representation. + /// + /// - Parameter image: The component’s preferred image representation. + /// - Parameter alternativeText: The component’s alternative text representation. Use this representation if the + /// image representation is unavailable or unusable, but consider formatting the text in a special way to + /// distinguish it from an ordinary ``VisualInstruction/Component/text(text:)`` component. + case image(image: ImageRepresentation, alternativeText: TextRepresentation) + + /// The component is an image of a zoomed junction, with a fallback text representation. + case guidanceView(image: GuidanceViewImageRepresentation, alternativeText: TextRepresentation) + + /// The component contains the localized word for “Exit”. + /// + /// This component may appear before or after an ``VisualInstruction/Component/exitCode(text:)`` component, + /// depending on the language. You can hide this component if the adjacent + /// ``VisualInstruction/Component/exitCode(text:)`` component has an obvious exit-number appearance, for example + /// with an accompanying [motorway exit + /// icon](https://commons.wikimedia.org/wiki/File:Sinnbild_Autobahnausfahrt.svg). + case exit(text: TextRepresentation) + + /// The component contains an exit number. + /// + /// You can hide the adjacent ``VisualInstruction/Component/exit(text:)`` component in favor of giving this + /// component an obvious exit-number appearance, for example by pairing it with a [motorway exit + /// icon](https://commons.wikimedia.org/wiki/File:Sinnbild_Autobahnausfahrt.svg). + case exitCode(text: TextRepresentation) + + /// A component that represents a turn lane or through lane at the approach to an intersection. + /// + /// - parameter indications: The direction or directions of travel that the lane is reserved for. + /// - parameter isUsable: Whether the user can use this lane to continue along the current route. + /// - parameter preferredDirection: Which of the `indications` is applicable to the current route when there is + /// more than one + case lane(indications: LaneIndication, isUsable: Bool, preferredDirection: ManeuverDirection?) + } +} + +extension VisualInstruction.Component { + /// A textual representation of a visual instruction component. + public struct TextRepresentation: Equatable, Sendable { + /// Initializes a text representation bearing the given abbreviatable text. + public init(text: String, abbreviation: String?, abbreviationPriority: Int?) { + self.text = text + self.abbreviation = abbreviation + self.abbreviationPriority = abbreviationPriority + } + + /// The plain text representation of this component. + public let text: String + + /// An abbreviated representation of the `text` property. + public let abbreviation: String? + + /// The priority for which the component should be abbreviated. + /// + /// A component with a lower abbreviation priority value should be abbreviated before a component with a higher + /// abbreviation priority value. + public let abbreviationPriority: Int? + } + + /// An image representation of a visual instruction component. + public struct ImageRepresentation: Equatable, Sendable { + /// File formats of visual instruction component images. + public enum Format: String, Sendable { + /// Portable Network Graphics (PNG) + case png + /// Scalable Vector Graphics (SVG) + case svg + } + + /// Initializes an image representation bearing the image at the given base URL. + public init(imageBaseURL: URL?, shield: ShieldRepresentation? = nil) { + self.imageBaseURL = imageBaseURL + self.shield = shield + } + + /// The URL whose path is the prefix of all the possible URLs returned by `imageURL(scale:format:)`. + public let imageBaseURL: URL? + + /// Optionally, a structured image representation for displaying a [highway + /// shield](https://en.wikipedia.org/wiki/Highway_shield). + public let shield: ShieldRepresentation? + + /// Returns a remote URL to the image file that represents the component. + /// - Parameters: + /// - scale: The image’s scale factor. If this argument is unspecified, the current screen’s native scale + /// factor is used. Only the values 1, 2, and 3 are currently supported. + /// - format: The file format of the image. If this argument is unspecified, PNG is used. + /// - Returns: A remote URL to the image. + public func imageURL(scale: Scale, format: Format = .png) -> URL? { + guard let imageBaseURL, + var imageURLComponents = URLComponents(url: imageBaseURL, resolvingAgainstBaseURL: false) + else { + return nil + } + imageURLComponents.path += "@\(Int(scale))x.\(format)" + return imageURLComponents.url + } + } + + /// A mapbox shield representation of a visual instruction component. + public struct ShieldRepresentation: Equatable, Codable, Sendable { + /// Initializes a mapbox shield with the given name, text color, and display ref. + public init(baseURL: URL, name: String, textColor: String, text: String) { + self.baseURL = baseURL + self.name = name + self.textColor = textColor + self.text = text + } + + /// Base URL to query the styles endpoint. + public let baseURL: URL + + /// String indicating the name of the route shield. + public let name: String + + /// String indicating the color of the text to be rendered on the route shield. + public let textColor: String + + /// String indicating the route reference code that will be displayed on the shield. + public let text: String + + private enum CodingKeys: String, CodingKey { + case baseURL = "base_url" + case name + case textColor = "text_color" + case text = "display_ref" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.baseURL = try container.decode(URL.self, forKey: .baseURL) + self.name = try container.decode(String.self, forKey: .name) + self.textColor = try container.decode(String.self, forKey: .textColor) + self.text = try container.decode(String.self, forKey: .text) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(baseURL, forKey: .baseURL) + try container.encode(name, forKey: .name) + try container.encode(textColor, forKey: .textColor) + try container.encode(text, forKey: .text) + } + } +} + +/// A guidance view image representation of a visual instruction component. +public struct GuidanceViewImageRepresentation: Equatable, Sendable { + /// Initializes an image representation bearing the image at the given URL. + public init(imageURL: URL?) { + self.imageURL = imageURL + } + + /// Returns a remote URL to the image file that represents the component. + public let imageURL: URL? +} + +extension VisualInstruction.Component: Codable { + private enum CodingKeys: String, CodingKey { + case kind = "type" + case text + case abbreviatedText = "abbr" + case abbreviatedTextPriority = "abbr_priority" + case imageBaseURL + case imageURL + case shield = "mapbox_shield" + case directions + case isActive = "active" + case activeDirection = "active_direction" + } + + enum Kind: String, Codable, Sendable { + case delimiter + case text + case image = "icon" + case guidanceView = "guidance-view" + case exit + case exitCode = "exit-number" + case lane + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let kind = (try? container.decode(Kind.self, forKey: .kind)) ?? .text + + if kind == .lane { + let indications = try container.decode(LaneIndication.self, forKey: .directions) + let isUsable = try container.decode(Bool.self, forKey: .isActive) + let preferredDirection = try container.decodeIfPresent(ManeuverDirection.self, forKey: .activeDirection) + self = .lane(indications: indications, isUsable: isUsable, preferredDirection: preferredDirection) + return + } + + let text = try container.decode(String.self, forKey: .text) + let abbreviation = try container.decodeIfPresent(String.self, forKey: .abbreviatedText) + let abbreviationPriority = try container.decodeIfPresent(Int.self, forKey: .abbreviatedTextPriority) + let textRepresentation = TextRepresentation( + text: text, + abbreviation: abbreviation, + abbreviationPriority: abbreviationPriority + ) + + switch kind { + case .delimiter: + self = .delimiter(text: textRepresentation) + case .text: + self = .text(text: textRepresentation) + case .image: + var imageBaseURL: URL? + if let imageBaseURLString = try container.decodeIfPresent(String.self, forKey: .imageBaseURL) { + imageBaseURL = URL(string: imageBaseURLString) + } + let shieldRepresentation = try container.decodeIfPresent(ShieldRepresentation.self, forKey: .shield) + let imageRepresentation = ImageRepresentation(imageBaseURL: imageBaseURL, shield: shieldRepresentation) + self = .image(image: imageRepresentation, alternativeText: textRepresentation) + case .exit: + self = .exit(text: textRepresentation) + case .exitCode: + self = .exitCode(text: textRepresentation) + case .lane: + preconditionFailure("Lane component should have been initialized before decoding text") + case .guidanceView: + var imageURL: URL? + if let imageURLString = try container.decodeIfPresent(String.self, forKey: .imageURL) { + imageURL = URL(string: imageURLString) + } + let guidanceViewImageRepresentation = GuidanceViewImageRepresentation(imageURL: imageURL) + self = .guidanceView(image: guidanceViewImageRepresentation, alternativeText: textRepresentation) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + let textRepresentation: TextRepresentation? + switch self { + case .delimiter(let text): + try container.encode(Kind.delimiter, forKey: .kind) + textRepresentation = text + case .text(let text): + try container.encode(Kind.text, forKey: .kind) + textRepresentation = text + case .image(let image, let alternativeText): + try container.encode(Kind.image, forKey: .kind) + textRepresentation = alternativeText + try container.encodeIfPresent(image.imageBaseURL?.absoluteString, forKey: .imageBaseURL) + try container.encodeIfPresent(image.shield, forKey: .shield) + case .exit(let text): + try container.encode(Kind.exit, forKey: .kind) + textRepresentation = text + case .exitCode(let text): + try container.encode(Kind.exitCode, forKey: .kind) + textRepresentation = text + case .lane(let indications, let isUsable, let preferredDirection): + try container.encode(Kind.lane, forKey: .kind) + textRepresentation = .init(text: "", abbreviation: nil, abbreviationPriority: nil) + try container.encode(indications, forKey: .directions) + try container.encode(isUsable, forKey: .isActive) + try container.encodeIfPresent(preferredDirection, forKey: .activeDirection) + case .guidanceView(let image, let alternativeText): + try container.encode(Kind.guidanceView, forKey: .kind) + textRepresentation = alternativeText + try container.encodeIfPresent(image.imageURL?.absoluteString, forKey: .imageURL) + } + + if let textRepresentation { + try container.encodeIfPresent(textRepresentation.text, forKey: .text) + try container.encodeIfPresent(textRepresentation.abbreviation, forKey: .abbreviatedText) + try container.encodeIfPresent(textRepresentation.abbreviationPriority, forKey: .abbreviatedTextPriority) + } + } +} + +extension VisualInstruction.Component { + public static func == (lhs: VisualInstruction.Component, rhs: VisualInstruction.Component) -> Bool { + switch (lhs, rhs) { + case (let .delimiter(lhsText), .delimiter(let rhsText)), + (let .text(lhsText), .text(let rhsText)), + (let .exit(lhsText), .exit(let rhsText)), + (let .exitCode(lhsText), .exitCode(let rhsText)): + return lhsText == rhsText + case ( + let .image(lhsURL, lhsAlternativeText), + .image(let rhsURL, let rhsAlternativeText) + ): + return lhsURL == rhsURL + && lhsAlternativeText == rhsAlternativeText + case ( + let .guidanceView(lhsURL, lhsAlternativeText), + .guidanceView(let rhsURL, let rhsAlternativeText) + ): + return lhsURL == rhsURL + && lhsAlternativeText == rhsAlternativeText + case ( + let .lane(lhsIndications, lhsIsUsable, lhsPreferredDirection), + .lane(let rhsIndications, let rhsIsUsable, let rhsPreferredDirection) + ): + return lhsIndications == rhsIndications + && lhsIsUsable == rhsIsUsable + && lhsPreferredDirection == rhsPreferredDirection + case (.delimiter, _), + (.text, _), + (.image, _), + (.exit, _), + (.exitCode, _), + (.guidanceView, _), + (.lane, _): + return false + } + } +} diff --git a/ios/Classes/Navigation/MapboxDirections/Waypoint.swift b/ios/Classes/Navigation/MapboxDirections/Waypoint.swift new file mode 100644 index 000000000..e583a4149 --- /dev/null +++ b/ios/Classes/Navigation/MapboxDirections/Waypoint.swift @@ -0,0 +1,349 @@ +#if canImport(CoreLocation) +import CoreLocation +#endif +import Turf + +/// A ``Waypoint`` object indicates a location along a route. It may be the route’s origin or destination, or it may be +/// another location that the route visits. A waypoint object indicates the location’s geographic location along with +/// other optional information, such as a name or the user’s direction approaching the waypoint. You create a +/// ``RouteOptions`` object using waypoint objects and also receive waypoint objects in the completion handler of the +/// `Directions.calculate(_:completionHandler:)` method. +public struct Waypoint: Codable, ForeignMemberContainer, Equatable, Sendable { + public var foreignMembers: JSONObject = [:] + + private enum CodingKeys: String, CodingKey, CaseIterable { + case coordinate = "location" + case coordinateAccuracy + case targetCoordinate + case heading + case headingAccuracy + case separatesLegs + case name + case allowsArrivingOnOppositeSide + case snappedDistance = "distance" + case layer + } + + // MARK: Creating a Waypoint + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.coordinate = try container.decode(LocationCoordinate2DCodable.self, forKey: .coordinate).decodedCoordinates + + self.coordinateAccuracy = try container.decodeIfPresent(LocationAccuracy.self, forKey: .coordinateAccuracy) + + self.targetCoordinate = try container.decodeIfPresent( + LocationCoordinate2DCodable.self, + forKey: .targetCoordinate + )?.decodedCoordinates + + self.heading = try container.decodeIfPresent(LocationDirection.self, forKey: .heading) + + self.headingAccuracy = try container.decodeIfPresent(LocationDirection.self, forKey: .headingAccuracy) + + if let separates = try container.decodeIfPresent(Bool.self, forKey: .separatesLegs) { + self.separatesLegs = separates + } + + if let allows = try container.decodeIfPresent(Bool.self, forKey: .allowsArrivingOnOppositeSide) { + self.allowsArrivingOnOppositeSide = allows + } + + if let name = try container.decodeIfPresent(String.self, forKey: .name), + !name.isEmpty + { + self.name = name + } else { + self.name = nil + } + + self.snappedDistance = try container.decodeIfPresent(LocationDistance.self, forKey: .snappedDistance) + + self.layer = try container.decodeIfPresent(Int.self, forKey: .layer) + + try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(LocationCoordinate2DCodable(coordinate), forKey: .coordinate) + try container.encodeIfPresent(coordinateAccuracy, forKey: .coordinateAccuracy) + let targetCoordinateCodable = targetCoordinate != nil ? LocationCoordinate2DCodable(targetCoordinate!) : nil + try container.encodeIfPresent(targetCoordinateCodable, forKey: .targetCoordinate) + try container.encodeIfPresent(heading, forKey: .heading) + try container.encodeIfPresent(headingAccuracy, forKey: .headingAccuracy) + try container.encodeIfPresent(separatesLegs, forKey: .separatesLegs) + try container.encodeIfPresent(allowsArrivingOnOppositeSide, forKey: .allowsArrivingOnOppositeSide) + try container.encodeIfPresent(name, forKey: .name) + try container.encodeIfPresent(snappedDistance, forKey: .snappedDistance) + try container.encodeIfPresent(layer, forKey: .layer) + + try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) + } + + /// Initializes a new waypoint object with the given geographic coordinate and an optional accuracy and name. + /// - Parameters: + /// - coordinate: The geographic coordinate of the waypoint. + /// - coordinateAccuracy: The maximum distance away from the waypoint that the route may come and still be + /// considered viable. This argument is measured in meters. A negative value means the route may be an indefinite + /// number of meters away from the route and still be considered viable. + /// It is recommended that the value of this argument be greater than the `horizontalAccuracy` property of a + /// `CLLocation` object obtained from a `CLLocationManager` object. There is a high likelihood that the user may be + /// located some distance away from a navigable road, for instance if the user is currently on a driveway or inside + /// a building. + /// - name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one + /// waypoint from another. + public init(coordinate: LocationCoordinate2D, coordinateAccuracy: LocationAccuracy? = nil, name: String? = nil) { + self.coordinate = coordinate + self.coordinateAccuracy = coordinateAccuracy + self.name = name + } + +#if canImport(CoreLocation) +#if os(tvOS) || os(watchOS) + /// Initializes a new waypoint object with the given `CLLocation` object and an optional heading value and name. + /// + /// - Note: This initializer is intended for `CLLocation` objects created using the + /// `CLLocation(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a + /// `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid + /// overfitting, since the ``Waypoint`` class’s `coordinateAccuracy` property represents the maximum allowed + /// deviation + /// from the waypoint. There is a high likelihood that the user may be located some distance away from a navigable + /// road, for instance if the user is currently on a driveway or inside a building. + /// - Parameters: + /// - location: A `CLLocation` object representing the waypoint’s location. This initializer respects the + /// `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `coordinate` and + /// `coordinateAccuracy` properties, respectively. + /// - heading: A `LocationDirection` value representing the direction from which the route must approach the + /// waypoint in order to be considered viable. This value is stored in the `headingAccuracy` property. + /// - name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one + /// waypoint from another. + public init(location: CLLocation, heading: LocationDirection? = nil, name: String? = nil) { + self.coordinate = location.coordinate + self.coordinateAccuracy = location.horizontalAccuracy + if let heading, heading >= 0 { + self.heading = heading + } + self.name = name + } +#else + /// Initializes a new waypoint object with the given `CLLocation` object and an optional `CLHeading` object and + /// name. + /// + /// - Note: This initializer is intended for `CLLocation` objects created using the + /// `CLLocation(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a + /// `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid + /// overfitting, since the ``Waypoint`` class’s ``Waypoint/coordinateAccuracy`` property represents the maximum + /// allowed deviation from the waypoint. There is a high likelihood that the user may be located some distance away + /// from a navigable road, for instance if the user is currently on a driveway of inside a building. + /// - Parameters: + /// - location: A `CLLocation` object representing the waypoint’s location. This initializer respects the + /// `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `coordinate` and + /// `coordinateAccuracy` properties, respectively. + /// - heading: A `CLHeading` object representing the direction from which the route must approach the waypoint in + /// order to be considered viable. This initializer respects the `CLHeading` class’s `trueHeading` property or + /// `magneticHeading` property, converting it into the `headingAccuracy` property. + /// - name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one + /// waypoint from another. + public init(location: CLLocation, heading: CLHeading? = nil, name: String? = nil) { + self.coordinate = location.coordinate + self.coordinateAccuracy = location.horizontalAccuracy + if let heading { + self.heading = heading.trueHeading >= 0 ? heading.trueHeading : heading.magneticHeading + } + self.name = name + } +#endif +#endif + + // MARK: Positioning the Waypoint + + /// The geographic coordinate of the waypoint. + public let coordinate: LocationCoordinate2D + + /// The radius of uncertainty for the waypoint, measured in meters. + /// + /// For a route to be considered viable, it must enter this waypoint’s circle of uncertainty. The ``coordinate`` + /// property identifies the center of the circle, while this property indicates the circle’s radius. If the value of + /// this property is negative, a route is considered viable regardless of whether it enters this waypoint’s circle + /// of uncertainty, subject to an undefined maximum distance. + /// + /// By default, the value of this property is `nil`. + public var coordinateAccuracy: LocationAccuracy? + + /// The geographic coordinate of the waypoint’s target. + /// + /// The waypoint’s target affects arrival instructions without affecting the route’s shape. For example, a delivery + /// or ride hailing application may specify a waypoint target that represents a drop-off location. The target + /// determines whether the arrival visual and spoken instructions indicate that the destination is “on the left” or + /// “on the right”. + /// + /// By default, this property is set to `nil`, meaning the waypoint has no target. This property is ignored on the + /// first waypoint of a ``RouteOptions`` object, on any waypoint of a ``MatchOptions`` object, or on any waypoint of + /// a ``RouteOptions`` object if ``DirectionsOptions/includesSteps`` is set to `false`. + /// + /// This property corresponds to the + /// [`waypoint_targets`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox + /// Directions and Map Matching APIs. + public var targetCoordinate: LocationCoordinate2D? + + /// A Boolean value indicating whether the waypoint may be snapped to a closed road in the resulting + /// ``RouteResponse``. + /// + /// If `true`, the waypoint may be snapped to a road segment that is closed due to a live traffic closure. This + /// property is `false` by default. This property corresponds to the [`snapping_include_closures`](https://docs.mapbox.com/api/navigation/directions/#optional-parameters-for-the-mapboxdriving-traffic-profile) + /// query parameter in the Mapbox Directions API. + public var allowsSnappingToClosedRoad: Bool = false + + /// A Boolean value indicating whether the waypoint may be snapped to a statically (long-term) closed road in the + /// resulting ``RouteResponse``. + /// + /// If `true`, the waypoint may be snapped to a road segment statically closed, that is long-term (for example, road + /// under construction). This property is `false` by default. This property corresponds to the [`snapping_include_static_closures`](https://docs.mapbox.com/api/navigation/directions/#optional-parameters-for-the-mapboxdriving-traffic-profile) + /// query parameter in the Mapbox Directions API. + public var allowsSnappingToStaticallyClosedRoad: Bool = false + + /// The straight-line distance from the coordinate specified in the query to the location it was snapped to in the + /// resulting ``RouteResponse``. + /// + /// By default, this property is set to `nil`, meaning the waypoint has no snapped distance. + public var snappedDistance: LocationDistance? + + /// The [layer](https://wiki.openstreetmap.org/wiki/Key:layer) of road that the waypoint is positioned which is used + /// to filter the road segment that the waypoint will be placed on in Z-order. It is useful for avoiding ambiguity + /// in the case of multi-level roads (such as a tunnel under a road). + /// + /// This property corresponds to the + /// [`layers`](https://docs.mapbox.com/api/navigation/directions/#optional-parameters) query parameter in the Mapbox + /// Directions API. If a matching layer is not found, the Mapbox Directions API will choose a suitable layer + /// according to the other provided ``DirectionsOptions`` and ``Waypoint`` properties. + /// + /// By default, this property is set to `nil`, meaning the route from the ``Waypoint`` will not be influenced by a + /// layer of road. + public var layer: Int? + + // MARK: Getting the Direction of Approach + + /// The direction from which a route must approach this waypoint in order to be considered viable. + /// + /// This property is measured in degrees clockwise from true north. A value of 0 degrees means due north, 90 degrees + /// means due east, 180 degrees means due south, and so on. If the value of this property is negative, a route is + /// considered viable regardless of the direction from which it approaches this waypoint. + /// + /// If this waypoint is the first waypoint (the source waypoint), the route must start out by heading in the + /// direction specified by this property. You should always set the ``headingAccuracy`` property in conjunction with + /// this property. If the ``headingAccuracy`` property is set to `nil`, this property is ignored. + /// + /// For driving directions, this property can be useful for avoiding a route that begins by going in the direction + /// opposite the current direction of travel. For example, if you know the user is moving eastwardly and the first + /// waypoint is the user’s current location, specifying a heading of 90 degrees and a heading accuracy of 90 degrees + /// for the first waypoint avoids a route that begins with a “head west” instruction. + /// + /// You should be certain that the user is in motion before specifying a heading and heading accuracy; otherwise, + /// you may be unnecessarily filtering out the best route. For example, suppose the user is sitting in a car parked + /// in a driveway, facing due north, with the garage in front and the street to the rear. In that case, specifying a + /// heading of 0 degrees and a heading accuracy of 90 degrees may result in a route that begins on the back alley + /// or, worse, no route at all. For this reason, it is recommended that you only specify a heading and heading + /// accuracy when automatically recalculating directions due to the user deviating from the route. + /// + /// By default, the value of this property is `nil`, meaning that a route is considered viable regardless of the + /// direction of approach. + public var heading: LocationDirection? = nil + + /// The maximum amount, in degrees, by which a route’s approach to a waypoint may differ from ``heading`` in either + /// direction in order to be considered viable. + /// + /// A value of 0 degrees means that the approach must match the specified ``heading`` exactly – an unlikely + /// scenario. A value of 180 degrees or more means that the approach may be as much as 180 degrees in either + /// direction from the specified ``heading``, effectively allowing a candidate route to approach the waypoint from + /// any direction. + /// + /// If you set the ``heading`` property, you should set this property to a value such as 90 degrees, to avoid + /// filtering out routes whose approaches differ only slightly from the specified `heading`. Otherwise, if the + /// ``heading`` property is set to a negative value, this property is ignored. + /// + /// By default, the value of this property is `nil`, meaning that a route is considered viable regardless of the + /// direction of approach. + public var headingAccuracy: LocationDirection? = nil + + var headingDescription: String { + guard let heading, heading >= 0, + let accuracy = headingAccuracy, accuracy >= 0 + else { + return "" + } + + return "\(heading.truncatingRemainder(dividingBy: 360)),\(min(accuracy, 180))" + } + + /// A Boolean value indicating whether arriving on opposite side is allowed. + /// + /// This property has no effect if ``DirectionsOptions/includesSteps`` is set to `false`. + /// This property corresponds to the + /// [`approaches`](https://www.mapbox.com/api-documentation/navigation/#retrieve-directions) query parameter in the + /// Mapbox Directions and Map Matching APIs. + public var allowsArrivingOnOppositeSide = true + + // MARK: Identifying the Waypoint + + /// The name of the waypoint. + /// + /// This property does not affect the route, but the name is included in the arrival instruction, to help the user + /// distinguish between multiple destinations. The name can also help you distinguish one waypoint from another in + /// the array of waypoints passed into the completion handler of the `Directions.calculate(_:completionHandler:)` + /// method. + public var name: String? + + // MARK: Separating the Routes Into Legs + + /// A Boolean value indicating whether the waypoint is significant enough to appear in the resulting routes as a + /// waypoint separating two legs, along with corresponding guidance instructions. + /// + /// By default, this property is set to `true`, which means that each resulting route will include a leg that ends + /// by arriving at the waypoint as ``RouteLeg/destination`` and a subsequent leg that begins by departing from the + /// waypoint as ``RouteLeg/source``. Otherwise, if this property is set to `false`, a single leg passes through the + /// waypoint without specifically mentioning it. Regardless of the value of this property, each resulting route + /// passes through the location specified by the ``coordinate`` property, accounting for approach-related properties + /// such as ``heading``. + /// + /// With the Mapbox Directions API, set this property to `false` if you want the waypoint’s location to influence + /// the path that the route follows without attaching any meaning to the waypoint object itself. With the Mapbox Map + /// Matching API, use this property when the ``DirectionsOptions/includesSteps`` property is `true` or when + /// ``coordinate`` represents a trace with a high sample rate. + /// + /// This property has no effect if ``DirectionsOptions/includesSteps`` is set to `false`, or if + /// ``MatchOptions/waypointIndices`` is non-nil. + /// This property corresponds to the [`approaches`](https://docs.mapbox.com/api/navigation/#retrieve-directions) + /// query parameter in the Mapbox Directions and Map Matching APIs. + public var separatesLegs: Bool = true +} + +extension Waypoint: CustomStringConvertible { + public var description: String { + return Mirror(reflecting: self).children.compactMap { + if let label = $0.label { + return "\(label): \($0.value)" + } + + return "" + }.joined(separator: "\n") + } +} + +#if canImport(CoreLocation) +extension Waypoint: CustomQuickLookConvertible { + func debugQuickLookObject() -> Any? { + return CLLocation( + coordinate: targetCoordinate ?? coordinate, + altitude: 0, + horizontalAccuracy: coordinateAccuracy ?? -1, + verticalAccuracy: -1, + course: heading ?? -1, + speed: -1, + timestamp: Date() + ) + } +} +#endif diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Billing/ApiConfiguration.swift b/ios/Classes/Navigation/MapboxNavigationCore/Billing/ApiConfiguration.swift new file mode 100644 index 000000000..61a33caa5 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Billing/ApiConfiguration.swift @@ -0,0 +1,78 @@ +import Foundation +import MapboxDirections + +/// The Mapbox access token specified in the main application bundle’s Info.plist. +private let defaultAccessToken: String? = + Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String ?? + Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAccessToken") as? String ?? + UserDefaults.standard.string(forKey: "MBXAccessToken") + +/// Configures access token for Mapbox API requests. +public struct ApiConfiguration: Sendable, Equatable { + /// The default configuration. The SDK will attempt to find an access token from your app's `Info.plist`. + public static var `default`: Self { + guard let defaultAccessToken, !defaultAccessToken.isEmpty else { + preconditionFailure( + "A Mapbox access token is required. Go to . In Info.plist, set the MBXAccessToken key to your access token." + ) + } + + return .init(accessToken: defaultAccessToken, endPoint: .mapboxApiEndpoint()) + } + + /// A Mapbox [access token](https://www.mapbox.com/help/define-access-token/) used to authorize Mapbox API requests. + public let accessToken: String + /// An optional hostname to the server API. Defaults to `api.mapbox.com`. + @_spi(MapboxInternal) + public let endPoint: URL + + /// Initializes ``ApiConfiguration`` instance. + /// - Parameters: + /// - accessToken: A Mapbox [access token](https://www.mapbox.com/help/define-access-token/) used to authorize + /// Mapbox API requests. + public init(accessToken: String) { + self.init(accessToken: accessToken, endPoint: nil) + } + + /// Initializes ``ApiConfiguration`` instance. + /// - Parameters: + /// - accessToken: A Mapbox [access token](https://www.mapbox.com/help/define-access-token/) used to authorize + /// Mapbox API requests. + /// - endPoint: An optional hostname to the server API. + @_spi(MapboxInternal) + public init( + accessToken: String, + endPoint: URL? + ) { + self.accessToken = accessToken + self.endPoint = endPoint ?? .mapboxApiEndpoint() + } + + init(requestURL url: URL) { + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + let accessToken = components? + .queryItems? + .first { $0.name == .accessTokenUrlQueryItemName }? + .value + components?.path = "" + components?.queryItems = nil + self.init( + accessToken: accessToken ?? defaultAccessToken!, + endPoint: components?.url ?? .mapboxApiEndpoint() + ) + } + + func accessTokenUrlQueryItem() -> URLQueryItem { + .init(name: .accessTokenUrlQueryItemName, value: accessToken) + } +} + +extension Credentials { + init(_ apiConfiguration: ApiConfiguration) { + self.init(accessToken: apiConfiguration.accessToken, host: apiConfiguration.endPoint.absoluteURL) + } +} + +extension String { + fileprivate static let accessTokenUrlQueryItemName: String = "access_token" +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler+SkuTokenProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler+SkuTokenProvider.swift new file mode 100644 index 000000000..55d863f62 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler+SkuTokenProvider.swift @@ -0,0 +1,9 @@ +import _MapboxNavigationHelpers + +extension BillingHandler { + func skuTokenProvider() -> SkuTokenProvider { + .init { + self.serviceSkuToken + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler.swift b/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler.swift new file mode 100644 index 000000000..9fee5fc86 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler.swift @@ -0,0 +1,477 @@ +// IMPORTANT: Tampering with any file that contains billing code is a violation of our ToS +// and will result in enforcement of the penalties stipulated in the ToS. + +import Foundation +import MapboxCommon_Private +import MapboxDirections + +/// Wrapper around `MapboxCommon_Private.BillingServiceFactory`, which provides its shared instance. +enum NativeBillingService { + /// Provides a new or an existing `MapboxCommon`s `BillingServiceFactory` instance. + static var shared: MapboxCommon_Private.BillingService { + MapboxCommon_Private.BillingServiceFactory.getInstance() + } +} + +/// BillingServiceError from MapboxCommon +private typealias BillingServiceErrorNative = MapboxCommon_Private.BillingServiceError + +/// Swift variant of `BillingServiceErrorNative` +enum BillingServiceError: Error { + /// Unknown error from Billing Service + case unknown + /// The request failed because the access token is invalid. + case tokenValidationFailed + /// The resume failed because the session doesn't exist or invalid. + case resumeFailed + + fileprivate init(_ nativeError: BillingServiceErrorNative) { + switch nativeError.code { + case .resumeFailed: + self = .resumeFailed + case .tokenValidationFailed: + self = .tokenValidationFailed + @unknown default: + self = .unknown + } + } +} + +/// Protocol for `NativeBillingService` implementation. Inversing the dependency on `NativeBillingService` allows us +/// to unit test our implementation. +protocol BillingService: Sendable { + func getSKUTokenIfValid(for sessionType: BillingHandler.SessionType) -> String + func beginBillingSession( + for sessionType: BillingHandler.SessionType, + onError: @escaping (BillingServiceError) -> Void + ) + func pauseBillingSession(for sessionType: BillingHandler.SessionType) + func resumeBillingSession( + for sessionType: BillingHandler.SessionType, + onError: @escaping (BillingServiceError) -> Void + ) + func stopBillingSession(for sessionType: BillingHandler.SessionType) + func triggerBillingEvent(onError: @escaping (BillingServiceError) -> Void) + func getSessionStatus(for sessionType: BillingHandler.SessionType) -> BillingHandler.SessionState +} + +/// Implementation of `BillingService` protocol which uses `NativeBillingService`. +private final class ProductionBillingService: BillingService { + /// `UserSKUIdentifier` which is used for navigation MAU billing events. + private let mauSku: UserSKUIdentifier = .nav3CoreMAU + private var sdkInformation: SdkInformation { + .init( + name: SdkInfo.navigationUX.name, + version: SdkInfo.navigationUX.version, + packageName: SdkInfo.navigationUX.packageName + ) + } + + init() {} + + func getSKUTokenIfValid(for sessionType: BillingHandler.SessionType) -> String { + NativeBillingService.shared.getSessionSKUTokenIfValid(for: tripSku(for: sessionType)) + } + + func beginBillingSession( + for sessionType: BillingHandler.SessionType, + onError: @escaping (BillingServiceError) -> Void + ) { + let skuToken = tripSku(for: sessionType) + Log.info("\(sessionType) billing session starts", category: .billing) + + NativeBillingService.shared.beginBillingSession( + for: sdkInformation, + skuIdentifier: skuToken, + callback: { + nativeBillingServiceError in + onError(BillingServiceError(nativeBillingServiceError)) + }, + validity: sessionType.maxSessionInterval + ) + } + + func pauseBillingSession(for sessionType: BillingHandler.SessionType) { + let skuToken = tripSku(for: sessionType) + Log.info("\(sessionType) billing session pauses", category: .billing) + NativeBillingService.shared.pauseBillingSession(for: skuToken) + } + + func resumeBillingSession( + for sessionType: BillingHandler.SessionType, + onError: @escaping (BillingServiceError) -> Void + ) { + let skuToken = tripSku(for: sessionType) + Log.info("\(sessionType) billing session resumes", category: .billing) + NativeBillingService.shared.resumeBillingSession(for: skuToken) { nativeBillingServiceError in + onError(BillingServiceError(nativeBillingServiceError)) + } + } + + func stopBillingSession(for sessionType: BillingHandler.SessionType) { + let skuToken = tripSku(for: sessionType) + Log.info("\(sessionType) billing session stops", category: .billing) + NativeBillingService.shared.stopBillingSession(for: skuToken) + } + + func triggerBillingEvent(onError: @escaping (BillingServiceError) -> Void) { + NativeBillingService.shared.triggerUserBillingEvent( + for: sdkInformation, + skuIdentifier: mauSku + ) { nativeBillingServiceError in + onError(BillingServiceError(nativeBillingServiceError)) + } + } + + func getSessionStatus(for sessionType: BillingHandler.SessionType) -> BillingHandler.SessionState { + switch NativeBillingService.shared.getSessionStatus(for: tripSku(for: sessionType)) { + case .noSession: return .stopped + case .sessionActive: return .running + case .sessionPaused: return .paused + @unknown default: + preconditionFailure("Unsupported session status from NativeBillingService.") + } + } + + private func tripSku(for sessionType: BillingHandler.SessionType) -> SessionSKUIdentifier { + switch sessionType { + case .activeGuidance: + return .nav3SesCoreAGTrip + case .freeDrive: + return .nav3SesCoreFDTrip + } + } +} + +/// Receives events about navigation changes and triggers appropriate events in `BillingService`. +/// +/// Session can be paused (`BillingHandler.pauseBillingSession(with:)`), stopped +/// (`BillingHandler.stopBillingSession(with:)`) or resumed (`BillingHandler.resumeBillingSession(with:)`). +/// +/// State of the billing sessions can be obtained using `BillingHandler.sessionState(uuid:)`. +final class BillingHandler: @unchecked Sendable { + /// Parameters on an active session. + private struct Session { + let type: SessionType + /// Indicates whether the session is active but paused. + var isPaused: Bool + } + + /// The state of the billing session. + enum SessionState: Equatable { + /// Indicates that there is no active billing session. + case stopped + /// There is an active paused billing session. + case paused + /// There is an active running billing session. + case running + } + + /// Supported session types. + enum SessionType: Equatable, CustomStringConvertible { + case freeDrive + case activeGuidance + + var maxSessionInterval: TimeInterval { + switch self { + case .activeGuidance: + return 43200 /* 12h */ + case .freeDrive: + return 3600 /* 1h */ + } + } + + var description: String { + switch self { + case .activeGuidance: + return "Active Guidance" + case .freeDrive: + return "Free Drive" + } + } + } + + static func createInstance(with accessToken: String?) -> BillingHandler { + precondition( + accessToken != nil, + "A Mapbox access token is required. Go to . In Info.plist, set the MBXAccessToken key to your access token." + ) + let service = ProductionBillingService() + return .init(service: service) + } + + /// The billing service which is used to send billing events. + private let billingService: BillingService + + /// A lock which serializes access to variables with underscore: `_sessions` etc. + /// As a convention, all class-level identifiers that starts with `_` should be executed with locked `lock`. + private let lock: NSLock = .init() + + /// All currently active sessions. Running or paused. When session is stopped, it is removed from this variable. + /// These sessions are different from `NativeBillingService` sessions. `BillingHandler.Session`s are mapped to one + /// `NativeBillingService`'s session for each `BillingHandler.SessionType`. + private var _sessions: [UUID: Session] = [:] + + /// The state of the billing session. + /// + /// - Important: This variable is safe to use from any thread. + /// - Parameter uuid: Session UUID which is provided in `BillingHandler.beginBillingSession(for:uuid:)`. + func sessionState(uuid: UUID) -> SessionState { + lock.lock(); defer { + lock.unlock() + } + + guard let session = _sessions[uuid] else { + return .stopped + } + + if session.isPaused { + return .paused + } else { + return .running + } + } + + func sessionType(uuid: UUID) -> SessionType? { + lock.lock(); defer { + lock.unlock() + } + + guard let session = _sessions[uuid] else { + return nil + } + return session.type + } + + /// The token to use for service requests like `Directions` etc. + var serviceSkuToken: String { + let sessionTypes: [BillingHandler.SessionType] = [.activeGuidance, .freeDrive] + + for sessionType in sessionTypes { + switch billingService.getSessionStatus(for: sessionType) { + case .running: + return billingService.getSKUTokenIfValid(for: sessionType) + case .paused, .stopped: + continue + } + } + + return "" + } + + private init(service: BillingService) { + self.billingService = service + } + + /// Starts a new billing session of the given `sessionType` identified by `uuid`. + /// + /// The `uuid` that is used to create a billing session must be provided in the following methods to perform + /// relevant changes to the started billing session: + /// - `BillingHandler.stopBillingSession(with:)` + /// - `BillingHandler.pauseBillingSession(with:)` + /// - `BillingHandler.resumeBillingSession(with:)` + /// + /// - Parameters: + /// - sessionType: The type of the billing session. + /// - uuid: The unique identifier of the billing session. + func beginBillingSession(for sessionType: SessionType, uuid: UUID) { + lock.lock() + + if var existingSession = _sessions[uuid] { + existingSession.isPaused = false + _sessions[uuid] = existingSession + } else { + let session = Session(type: sessionType, isPaused: false) + _sessions[uuid] = session + } + + let sessionStatus = billingService.getSessionStatus(for: sessionType) + + lock.unlock() + + switch sessionStatus { + case .stopped: + billingService.triggerBillingEvent(onError: { _ in + Log.fault("MAU isn't counted", category: .billing) + }) + billingService.beginBillingSession(for: sessionType, onError: { [weak self] error in + Log.fault( + "Trip session isn't started. Please check that you have the correct Mapboox Access Token", + category: .billing + ) + + switch error { + case .tokenValidationFailed: + assertionFailure( + "Token validation failed. Please check that you have the correct Mapbox Access Token." + ) + case .resumeFailed, .unknown: + break + } + self?.failedToBeginBillingSession(with: uuid, with: error) + }) + case .paused: + resumeBillingSession(with: uuid) + case .running: + break + } + } + + /// Starts a new billing session in `billingService` if a session with `uuid` exists. + /// + /// Use this method to force `billingService` to start a new billing session. + func beginNewBillingSessionIfExists(with uuid: UUID) { + lock.lock() + + guard let session = _sessions[uuid] else { + lock.unlock(); return + } + + lock.unlock() + + billingService.beginBillingSession(for: session.type) { error in + Log.fault( + "New trip session isn't started. Please check that you have the correct Mapboox Access Token.", + category: .billing + ) + + switch error { + case .tokenValidationFailed: + assertionFailure( + "Token validation failed. Please check that you have the correct Mapboox Access Token." + ) + case .resumeFailed, .unknown: + break + } + } + + if session.isPaused { + pauseBillingSession(with: uuid) + } + } + + /// Stops the billing session identified by the `uuid`. + func stopBillingSession(with uuid: UUID) { + lock.lock() + guard let session = _sessions[uuid] else { + lock.unlock(); return + } + _sessions[uuid] = nil + + let hasSessionWithSameType = _hasSession(with: session.type) + let triggerStopSessionEvent = !hasSessionWithSameType + && billingService.getSessionStatus(for: session.type) != .stopped + let triggerPauseSessionEvent = + !triggerStopSessionEvent + && hasSessionWithSameType + && !_hasSession(with: session.type, isPaused: false) + && billingService.getSessionStatus(for: session.type) != .paused + lock.unlock() + + if triggerStopSessionEvent { + billingService.stopBillingSession(for: session.type) + } else if triggerPauseSessionEvent { + billingService.pauseBillingSession(for: session.type) + } + } + + /// Pauses the billing session identified by the `uuid`. + func pauseBillingSession(with uuid: UUID) { + lock.lock() + guard var session = _sessions[uuid] else { + assertionFailure("Trying to pause non-existing session.") + lock.unlock(); return + } + session.isPaused = true + _sessions[uuid] = session + + let triggerBillingServiceEvent = !_hasSession(with: session.type, isPaused: false) + && billingService.getSessionStatus(for: session.type) == .running + lock.unlock() + + if triggerBillingServiceEvent { + billingService.pauseBillingSession(for: session.type) + } + } + + /// Resumes the billing session identified by the `uuid`. + func resumeBillingSession(with uuid: UUID) { + lock.lock() + guard var session = _sessions[uuid] else { + assertionFailure("Trying to resume non-existing session.") + lock.unlock(); return + } + session.isPaused = false + _sessions[uuid] = session + let triggerBillingServiceEvent = billingService.getSessionStatus(for: session.type) == .paused + lock.unlock() + + if triggerBillingServiceEvent { + billingService.resumeBillingSession(for: session.type) { _ in + self.failedToResumeBillingSession(with: uuid) + } + } + } + + func shouldStartNewBillingSession(for newRoute: Route, remainingWaypoints: [Waypoint]) -> Bool { + let newRouteWaypoints = newRoute.legs.compactMap(\.destination) + + guard !newRouteWaypoints.isEmpty else { + return false // Don't need to bil for routes without waypoints + } + + guard newRouteWaypoints.count == remainingWaypoints.count else { + Log.info( + "A new route is about to be set with a different set of waypoints, leading to the initiation of a new Active Guidance trip. For more information, see the “[Pricing](https://docs.mapbox.com/ios/beta/navigation/guides/pricing/)” guide.", + category: .billing + ) + return true + } + + for (newWaypoint, currentWaypoint) in zip(newRouteWaypoints, remainingWaypoints) { + if newWaypoint.coordinate.distance(to: currentWaypoint.coordinate) > 100 { + Log.info( + "A new route waypoint \(newWaypoint) is further than 100 meters from current waypoint \(currentWaypoint), leading to the initiation of a new Active Guidance trip. For more information, see the “[Pricing](https://docs.mapbox.com/ios/navigation/guides/pricing/)” guide. ", + category: .billing + ) + return true + } + } + + return false + } + + private func failedToBeginBillingSession(with uuid: UUID, with error: Error) { + lock { + _sessions[uuid] = nil + } + } + + private func failedToResumeBillingSession(with uuid: UUID) { + lock.lock() + guard let session = _sessions[uuid] else { + lock.unlock(); return + } + _sessions[uuid] = nil + lock.unlock() + beginBillingSession(for: session.type, uuid: uuid) + } + + private func _hasSession(with type: SessionType) -> Bool { + return _sessions.contains(where: { $0.value.type == type }) + } + + private func _hasSession(with type: SessionType, isPaused: Bool) -> Bool { + return _sessions.values.contains { session in + session.type == type && session.isPaused == isPaused + } + } +} + +// MARK: - Tests Support + +extension BillingHandler { + static func __createMockedHandler(with service: BillingService) -> BillingHandler { + BillingHandler(service: service) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Billing/SkuTokenProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Billing/SkuTokenProvider.swift new file mode 100644 index 000000000..60a24d578 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Billing/SkuTokenProvider.swift @@ -0,0 +1,7 @@ +public struct SkuTokenProvider: Sendable { + public let skuToken: @Sendable () -> String? + + public init(skuToken: @Sendable @escaping () -> String?) { + self.skuToken = skuToken + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Cache/FileCache.swift b/ios/Classes/Navigation/MapboxNavigationCore/Cache/FileCache.swift new file mode 100644 index 000000000..3cefc84fd --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Cache/FileCache.swift @@ -0,0 +1,92 @@ +import Foundation + +final class FileCache: Sendable { + typealias CompletionHandler = @Sendable () -> Void + + let diskCacheURL: URL = { + let fileManager = FileManager.default + let basePath = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first! + let identifier = Bundle.mapboxNavigationUXCore.bundleIdentifier! + return basePath.appendingPathComponent(identifier + ".downloadedFiles") + }() + + let diskAccessQueue = DispatchQueue(label: Bundle.mapboxNavigationUXCore.bundleIdentifier! + ".diskAccess") + + /// Stores data in the file cache for the given key, and calls the completion handler when finished. + public func store(_ data: Data, forKey key: String, completion: CompletionHandler? = nil) { + diskAccessQueue.async { + self.createCacheDirIfNeeded(self.diskCacheURL) + let cacheURL = self.cacheURLWithKey(key) + + do { + try data.write(to: cacheURL) + } catch { + Log.error( + "Failed to write data to URL \(cacheURL)", + category: .navigationUI + ) + } + completion?() + } + } + + /// Returns data from the file cache for the given key + public func data(forKey key: String) -> Data? { + let cacheKey = cacheURLWithKey(key) + do { + return try diskAccessQueue.sync { + try Data(contentsOf: cacheKey) + } + } catch { + return nil + } + } + + /// Clears the disk cache by removing and recreating the cache directory, and calls the completion handler when + /// finished. + public func clearDisk(completion: CompletionHandler? = nil) { + let cacheURL = diskCacheURL + diskAccessQueue.async { + do { + let fileManager = FileManager() + try fileManager.removeItem(at: cacheURL) + } catch { + Log.error( + "Failed to remove cache dir: \(cacheURL)", + category: .navigationUI + ) + } + + self.createCacheDirIfNeeded(cacheURL) + + completion?() + } + } + + private func cacheURLWithKey(_ key: String) -> URL { + let cacheKey = cacheKeyForKey(key) + return diskCacheURL.appendingPathComponent(cacheKey) + } + + private func cacheKeyForKey(_ key: String) -> String { + key.sha256 + } + + private func createCacheDirIfNeeded(_ url: URL) { + let fileManager = FileManager() + if fileManager.fileExists(atPath: url.absoluteString) == false { + do { + try fileManager.createDirectory( + at: url, + withIntermediateDirectories: true, + attributes: nil + ) + } catch { + Log.error( + "Failed to create directory: \(url)", + category: .navigationUI + ) + } + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Cache/SyncBimodalCache.swift b/ios/Classes/Navigation/MapboxNavigationCore/Cache/SyncBimodalCache.swift new file mode 100644 index 000000000..bfbdb6e18 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Cache/SyncBimodalCache.swift @@ -0,0 +1,85 @@ +import Foundation +import UIKit + +protocol SyncBimodalCache { + func clear(mode: CacheMode) + func store(data: Data, key: String, mode: CacheMode) + + subscript(key: String) -> Data? { get } +} + +struct CacheMode: OptionSet { + var rawValue: Int + + init(rawValue: Int) { + self.rawValue = rawValue + } + + static let InMemory = CacheMode(rawValue: 1 << 0) + static let OnDisk = CacheMode(rawValue: 1 << 1) +} + +final class MapboxSyncBimodalCache: SyncBimodalCache, @unchecked Sendable { + private let accessLock: NSLock + private let memoryCache: NSCache + private let fileCache = FileCache() + + public init() { + self.accessLock = .init() + self.memoryCache = NSCache() + memoryCache.name = "In-Memory Data Cache" + + DispatchQueue.main.async { + NotificationCenter.default.addObserver( + self, + selector: #selector(self.clearMemory), + name: UIApplication.didReceiveMemoryWarningNotification, + object: nil + ) + } + } + + @objc + func clearMemory() { + accessLock.withLock { + memoryCache.removeAllObjects() + } + } + + func clear(mode: CacheMode) { + accessLock.withLock { + if mode.contains(.InMemory) { + memoryCache.removeAllObjects() + } else if mode.contains(.OnDisk) { + fileCache.clearDisk() + } + } + } + + func store(data: Data, key: String, mode: CacheMode) { + accessLock.withLock { + if mode.contains(.InMemory) { + memoryCache.setObject( + data as NSData, + forKey: key as NSString + ) + } else if mode.contains(.OnDisk) { + fileCache.store( + data, + forKey: key + ) + } + } + } + + subscript(key: String) -> Data? { + accessLock.withLock { + return memoryCache.object( + forKey: key as NSString + ) as Data? ?? + fileCache.data( + forKey: key + ) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/CoreConstants.swift b/ios/Classes/Navigation/MapboxNavigationCore/CoreConstants.swift new file mode 100644 index 000000000..0ef99dd68 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/CoreConstants.swift @@ -0,0 +1,190 @@ +import Foundation + +extension Notification.Name { + // MARK: Switching Navigation Tile Versions + + /// Posted when Navigator has not enough tiles for map matching on current tiles version, but there are suitable + /// older versions inside underlying Offline Regions. Navigator has restarted when this notification is issued. + /// + /// Such action invalidates all existing matched ``RoadObject`` which should be re-applied manually. + /// The user info dictionary contains the key ``Navigator/NotificationUserInfoKey/tilesVersionKey`` + @_documentation(visibility: internal) + public static let navigationDidSwitchToFallbackVersion: Notification + .Name = .init(rawValue: "NavigatorDidFallbackToOfflineVersion") + + /// Posted when Navigator was switched to a fallback offline tiles version, but latest tiles became available again. + /// Navigator has restarted when this notification is issued. + /// Such action invalidates all existing matched ``RoadObject``s which should be re-applied manually. + /// The user info dictionary contains the key ``NativeNavigator/NotificationUserInfoKey/tilesVersionKey`` + @_documentation(visibility: internal) + public static let navigationDidSwitchToTargetVersion: Notification + .Name = .init(rawValue: "NavigatorDidRestoreToOnlineVersion") + + /// Posted when NavNative sends updated navigation status. + /// + /// The user info dictionary contains the keys ``Navigator.NotificationUserInfoKey.originKey`` and + /// ``Navigator/NotificationUserInfoKey/statusKey``. + static let navigationStatusDidChange: Notification.Name = .init(rawValue: "NavigationStatusDidChange") +} + +extension Notification.Name { + // MARK: Handling Alternative Routes + + static let navigatorDidChangeAlternativeRoutes: Notification + .Name = .init(rawValue: "NavigatorDidChangeAlternativeRoutes") + + static let navigatorDidFailToChangeAlternativeRoutes: Notification + .Name = .init(rawValue: "NavigatorDidFailToChangeAlternativeRoutes") + + static let navigatorWantsSwitchToCoincideOnlineRoute: Notification + .Name = .init(rawValue: "NavigatorWantsSwitchToCoincideOnlineRoute") +} + +extension Notification.Name { + // MARK: Electronic Horizon Notifications + + /// Posted when the user’s position in the electronic horizon changes. This notification may be posted multiple + /// times after ``Foundation/NSNotification/Name/electronicHorizonDidEnterRoadObject`` until the user transitions to + /// a new electronic horizon. + /// + /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/positionKey``, + /// ``RoadGraph/NotificationUserInfoKey/treeKey``, ``RoadGraph/NotificationUserInfoKey/updatesMostProbablePathKey``, + /// and ``RoadGraph/NotificationUserInfoKey/distancesByRoadObjectKey``. + public static let electronicHorizonDidUpdatePosition: Notification.Name = + .init(rawValue: "ElectronicHorizonDidUpdatePosition") + + /// Posted when the user enters a linear road object. + /// + /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/roadObjectIdentifierKey`` and + /// ``RoadGraph/NotificationUserInfoKey/didTransitionAtEndpointKey``. + public static let electronicHorizonDidEnterRoadObject: Notification.Name = + .init(rawValue: "ElectronicHorizonDidEnterRoadObject") + + /// Posted when the user exits a linear road object. + /// + /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/roadObjectIdentifierKey`` and + /// ``RoadGraph/NotificationUserInfoKey/didTransitionAtEndpointKey``. + public static let electronicHorizonDidExitRoadObject: Notification.Name = + .init(rawValue: "ElectronicHorizonDidExitRoadObject") + + /// Posted when user has passed point-like object. + /// + /// The user info dictionary contains the key ``RoadGraph/NotificationUserInfoKey/roadObjectIdentifierKey``. + public static let electronicHorizonDidPassRoadObject: Notification.Name = + .init(rawValue: "ElectronicHorizonDidPassRoadObject") +} + +extension Notification.Name { + // MARK: Route Refreshing Notifications + + /// Posted when the user’s position in the electronic horizon changes. This notification may be posted multiple + /// times after ``electronicHorizonDidEnterRoadObject`` until the user transitions to a new electronic horizon. + /// + /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/positionKey``, + /// ``RoadGraph/NotificationUserInfoKey/treeKey``, ``RoadGraph/NotificationUserInfoKey/updatesMostProbablePathKey``, + /// and ``RoadGraph/NotificationUserInfoKey/distancesByRoadObjectKey``. + static let routeRefreshDidUpdateAnnotations: Notification.Name = .init(rawValue: "RouteRefreshDidUpdateAnnotations") + + /// Posted when the user enters a linear road object. + /// + /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/roadObjectIdentifierKey`` and + /// ``RoadGraph/NotificationUserInfoKey/didTransitionAtEndpointKey``. + static let routeRefreshDidCancelRefresh: Notification.Name = .init(rawValue: "RouteRefreshDidCancelRefresh") + + /// Posted when the user exits a linear road object. + /// + /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/roadObjectIdentifierKey`` and + /// ``RoadGraph.NotificationUserInfoKey.transitionKey``. + static let routeRefreshDidFailRefresh: Notification.Name = .init(rawValue: "RouteRefreshDidFailRefresh") +} + +extension NativeNavigator { + /// Keys in the user info dictionaries of various notifications posted by instances of `NativeNavigator`. + public struct NotificationUserInfoKey: Hashable, Equatable, RawRepresentable { + public typealias RawValue = String + public var rawValue: String + public init(rawValue: String) { + self.rawValue = rawValue + } + + static let refreshRequestIdKey: NotificationUserInfoKey = .init(rawValue: "refreshRequestId") + static let refreshedRoutesResultKey: NotificationUserInfoKey = .init(rawValue: "refreshedRoutesResultKey") + static let legIndexKey: NotificationUserInfoKey = .init(rawValue: "legIndex") + static let refreshRequestErrorKey: NotificationUserInfoKey = .init(rawValue: "refreshRequestError") + + /// A key in the user info dictionary of a + /// ``Foundation/NSNotification/Name/navigationDidSwitchToFallbackVersion`` or + /// ``Foundation/NSNotification/Name/navigationDidSwitchToTargetVersion`` notification. The corresponding value + /// is a string representation of selected tiles version. + /// + /// For internal use only. + @_documentation(visibility: internal) + public static let tilesVersionKey: NotificationUserInfoKey = .init(rawValue: "tilesVersion") + + static let originKey: NotificationUserInfoKey = .init(rawValue: "origin") + + static let statusKey: NotificationUserInfoKey = .init(rawValue: "status") + + static let alternativesListKey: NotificationUserInfoKey = .init(rawValue: "alternativesList") + + static let removedAlternativesKey: NotificationUserInfoKey = .init(rawValue: "removedAlternatives") + + static let messageKey: NotificationUserInfoKey = .init(rawValue: "message") + + static let coincideOnlineRouteKey: NotificationUserInfoKey = .init(rawValue: "coincideOnlineRoute") + } +} + +extension RoadGraph { + /// Keys in the user info dictionaries of various notifications posted about ``RoadGraph``s. + public struct NotificationUserInfoKey: Hashable, Equatable, RawRepresentable, Sendable { + public typealias RawValue = String + public var rawValue: String + public init(rawValue: String) { + self.rawValue = rawValue + } + + /// A key in the user info dictionary of a ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition`` + /// notification. The corresponding value is a ``RoadGraph/Position`` indicating the current position in the + /// road graph. + public static let positionKey: NotificationUserInfoKey = .init(rawValue: "position") + + /// A key in the user info dictionary of a ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition`` + /// notification. The corresponding value is an ``RoadGraph/Edge`` at the root of a tree of edges in the routing + /// graph. This graph represents a probable path (or paths) of a vehicle within the routing graph for a certain + /// distance in front of the vehicle, thus extending the user’s perspective beyond the “visible” horizon as the + /// vehicle’s position and trajectory change. + public static let treeKey: NotificationUserInfoKey = .init(rawValue: "tree") + + /// A key in the user info dictionary of a ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition`` + /// notification. The corresponding value is a Boolean value of `true` if the position update indicates a new + /// most probable path (MPP) or `false` if it updates an existing MPP that the user has continued to follow. + /// + /// An electronic horizon can represent a new MPP in three scenarios: + /// - An electronic horizon is detected for the very first time. + /// - A user location tracking error leads to an MPP completely distinct from the previous MPP. + /// - The user has departed from the previous MPP, for example by driving to a side path of the previous MPP. + public static let updatesMostProbablePathKey: NotificationUserInfoKey = + .init(rawValue: "updatesMostProbablePath") + + /// A key in the user info dictionary of a ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition`` + /// notification. The corresponding value is an array of upcoming road object distances from the user’s current + /// location as ``DistancedRoadObject`` values. + public static let distancesByRoadObjectKey: NotificationUserInfoKey = .init(rawValue: "distancesByRoadObject") + + /// A key in the user info dictionary of a + /// ``Foundation/NSNotification/Name/electronicHorizonDidEnterRoadObject`` or + /// ``Foundation/NSNotification/Name/electronicHorizonDidExitRoadObject`` notification. The corresponding value + /// is a + /// ``RoadObject/Identifier`` identifying the road object that the user entered or exited. + public static let roadObjectIdentifierKey: NotificationUserInfoKey = .init(rawValue: "roadObjectIdentifier") + + /// A key in the user info dictionary of a + /// ``Foundation/NSNotification/Name/electronicHorizonDidEnterRoadObject`` or + /// ``Foundation/NSNotification/Name/electronicHorizonDidExitRoadObject`` notification. The corresponding value + /// is an `NSNumber` containing a Boolean value set to `true` if the user entered at the beginning or exited at + /// the end of the road object, or `false` if they entered or exited somewhere along the road object. + public static let didTransitionAtEndpointKey: NotificationUserInfoKey = + .init(rawValue: "didTransitionAtEndpoint") + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Environment.swift b/ios/Classes/Navigation/MapboxNavigationCore/Environment.swift new file mode 100644 index 000000000..334a2fd86 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Environment.swift @@ -0,0 +1,13 @@ +struct Environment: Sendable { + var audioPlayerClient: AudioPlayerClient +} + +extension Environment { + @AudioPlayerActor + static let live = Environment( + audioPlayerClient: .liveValue() + ) +} + +@AudioPlayerActor +var Current = Environment.live diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AVAudioSession.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AVAudioSession.swift new file mode 100644 index 000000000..0e6cd76af --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AVAudioSession.swift @@ -0,0 +1,27 @@ +import AVFoundation + +extension AVAudioSession { + // MARK: Adjusting the Volume + + public func tryDuckAudio() -> Error? { + do { + try setCategory(.playback, mode: .voicePrompt, options: [.duckOthers, .mixWithOthers]) + try setActive(true) + } catch { + return error + } + return nil + } + + public func tryUnduckAudio() -> Error? { + do { + try setActive( + false, + options: [.notifyOthersOnDeactivation] + ) + } catch { + return error + } + return nil + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AmenityType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AmenityType.swift new file mode 100644 index 000000000..411668eaa --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AmenityType.swift @@ -0,0 +1,51 @@ +import MapboxDirections +import MapboxNavigationNative + +extension MapboxDirections.AmenityType { + init(_ native: MapboxNavigationNative.AmenityType) { + switch native { + case .undefined: + self = .undefined + case .gasStation: + self = .gasStation + case .electricChargingStation: + self = .electricChargingStation + case .toilet: + self = .toilet + case .coffee: + self = .coffee + case .restaurant: + self = .restaurant + case .snack: + self = .snack + case .ATM: + self = .ATM + case .info: + self = .info + case .babyCare: + self = .babyCare + case .facilitiesForDisabled: + self = .facilitiesForDisabled + case .shop: + self = .shop + case .telephone: + self = .telephone + case .hotel: + self = .hotel + case .hotspring: + self = .hotSpring + case .shower: + self = .shower + case .picnicShelter: + self = .picnicShelter + case .post: + self = .post + case .FAX: + self = .fax + @unknown default: + self = .undefined + Log.fault("Unexpected amenity type.", category: .navigation) + assertionFailure("Unexpected amenity type.") + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Array++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Array++.swift new file mode 100644 index 000000000..5c5eac758 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Array++.swift @@ -0,0 +1,37 @@ +import CoreLocation +import Turf + +@_spi(MapboxInternal) +extension Array where Iterator.Element == CLLocationCoordinate2D { + public func sliced( + from: CLLocationCoordinate2D? = nil, + to: CLLocationCoordinate2D? = nil + ) -> [CLLocationCoordinate2D] { + return LineString(self).sliced(from: from, to: to)?.coordinates ?? [] + } + + public func distance( + from: CLLocationCoordinate2D? = nil, + to: CLLocationCoordinate2D? = nil + ) -> CLLocationDistance? { + return LineString(self).distance(from: from, to: to) + } + + public func trimmed( + from: CLLocationCoordinate2D? = nil, + distance: CLLocationDistance + ) -> [CLLocationCoordinate2D] { + if let fromCoord = from ?? first { + return LineString(self).trimmed(from: fromCoord, distance: distance)?.coordinates ?? [] + } else { + return [] + } + } + + public var centerCoordinate: CLLocationCoordinate2D { + let avgLat = map(\.latitude).reduce(0.0, +) / Double(count) + let avgLng = map(\.longitude).reduce(0.0, +) / Double(count) + + return CLLocationCoordinate2D(latitude: avgLat, longitude: avgLng) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/BoundingBox++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/BoundingBox++.swift new file mode 100644 index 000000000..36e09cb17 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/BoundingBox++.swift @@ -0,0 +1,15 @@ +import CoreGraphics +import Turf + +extension BoundingBox { + /// Returns zoom level inside of specific `CGSize`, in which `BoundingBox` was fit to. + func zoomLevel(fitTo size: CGSize) -> Double { + let latitudeFraction = (northEast.latitude.toRadians() - southWest.latitude.toRadians()) / .pi + let longitudeDiff = northEast.longitude - southWest.longitude + let longitudeFraction = ((longitudeDiff < 0) ? (longitudeDiff + 360) : longitudeDiff) / 360 + let latitudeZoom = log(Double(size.height) / 512.0 / latitudeFraction) / M_LN2 + let longitudeZoom = log(Double(size.width) / 512.0 / longitudeFraction) / M_LN2 + + return min(latitudeZoom, longitudeZoom, 21.0) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Bundle.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Bundle.swift new file mode 100644 index 000000000..134956b2e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Bundle.swift @@ -0,0 +1,63 @@ +import Foundation +import UIKit + +private final class BundleToken {} + +extension Bundle { + // MARK: Accessing Mapbox-Specific Bundles + + /// Returns a set of strings containing supported background mode types. + public var backgroundModes: Set { + if let modes = object(forInfoDictionaryKey: "UIBackgroundModes") as? [String] { + return Set(modes) + } + return [] + } + + var locationAlwaysAndWhenInUseUsageDescription: String? { + return object(forInfoDictionaryKey: "NSLocationAlwaysAndWhenInUseUsageDescription") as? String + } + + var locationWhenInUseUsageDescription: String? { + return object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") as? String + } + +#if !SWIFT_PACKAGE + private static let module: Bundle = .init(for: BundleToken.self) +#endif + + /// The Mapbox Core Navigation framework bundle. + public static let mapboxNavigationUXCore: Bundle = .module + + /// Provides `Bundle` instance, based on provided bundle name and class inside of it. + /// - Parameters: + /// - bundleName: Name of the bundle. + /// - class: Class, which is located inside of the bundle. + /// - Returns: Instance of the bundle if it was found, otherwise `nil`. + static func bundle(for bundleName: String, class: AnyClass) -> Bundle? { + let candidates = [ + // Bundle should be present here when the package is linked into an App. + Bundle.main.resourceURL, + + // Bundle should be present here when the package is linked into a framework. + Bundle(for: `class`).resourceURL, + ] + + for candidate in candidates { + let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle") + if let bundle = bundlePath.flatMap(Bundle.init(url:)) { + return bundle + } + } + + return nil + } + + public func image(named: String) -> UIImage? { + guard let image = UIImage(named: named, in: self, compatibleWith: nil) else { + assertionFailure("Image \(named) wasn't found in Core Framework bundle") + return nil + } + return image + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CLLocationDirection++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CLLocationDirection++.swift new file mode 100644 index 000000000..40479f639 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CLLocationDirection++.swift @@ -0,0 +1,9 @@ +import CoreLocation + +extension CLLocationDirection { + /// Returns shortest rotation between two angles. + func shortestRotation(angle: CLLocationDirection) -> CLLocationDirection { + guard !isNaN, !angle.isNaN else { return 0.0 } + return (self - angle).wrap(min: -180.0, max: 180.0) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CongestionLevel.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CongestionLevel.swift new file mode 100644 index 000000000..10ecb2896 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CongestionLevel.swift @@ -0,0 +1,118 @@ +import CarPlay +import Foundation +import MapboxDirections + +/// Range of numeric values determining congestion level. +/// +/// Congestion ranges work with `NumericCongestionLevel` values that can be requested by specifying +/// `AttributeOptions.numericCongestionLevel` in `DirectionOptions.attributes` when making Directions request. +public typealias CongestionRange = Range + +/// Configuration for connecting numeric congestion values to range categories. +public struct CongestionRangesConfiguration: Equatable, Sendable { + /// Numeric range for low congestion. + public var low: CongestionRange + /// Numeric range for moderate congestion. + public var moderate: CongestionRange + /// Numeric range for heavy congestion. + public var heavy: CongestionRange + /// Numeric range for severe congestion. + public var severe: CongestionRange + + /// Creates a new ``CongestionRangesConfiguration`` instance. + public init(low: CongestionRange, moderate: CongestionRange, heavy: CongestionRange, severe: CongestionRange) { + precondition(low.lowerBound >= 0, "Congestion level ranges can't include negative values.") + precondition( + low.upperBound <= moderate.lowerBound, + "Values from the moderate congestion level range can't intersect with or be lower than ones from the low congestion level range." + ) + precondition( + moderate.upperBound <= heavy.lowerBound, + "Values from the heavy congestion level range can't intersect with or be lower than ones from the moderate congestion level range." + ) + precondition( + heavy.upperBound <= severe.lowerBound, + "Values from the severe congestion level range can't intersect with or be lower than ones from the heavy congestion level range." + ) + precondition(severe.upperBound <= 101, "Congestion level ranges can't include values greater than 100.") + + self.low = low + self.moderate = moderate + self.heavy = heavy + self.severe = severe + } + + /// Default congestion ranges configuration. + public static var `default`: Self { + .init( + low: 0..<40, + moderate: 40..<60, + heavy: 60..<80, + severe: 80..<101 + ) + } +} + +extension CongestionLevel { + init(numericValue: NumericCongestionLevel?, configuration: CongestionRangesConfiguration) { + guard let numericValue else { + self = .unknown + return + } + + switch numericValue { + case configuration.low: + self = .low + case configuration.moderate: + self = .moderate + case configuration.heavy: + self = .heavy + case configuration.severe: + self = .severe + default: + self = .unknown + } + } + + /// Converts a CongestionLevel to a CPTimeRemainingColor. + public var asCPTimeRemainingColor: CPTimeRemainingColor { + switch self { + case .unknown: + return .default + case .low: + return .green + case .moderate: + return .orange + case .heavy: + return .red + case .severe: + return .red + } + } +} + +extension RouteLeg { + /// An array containing the traffic congestion level along each road segment in the route leg geometry. + /// + /// The array is formed either by converting values of `segmentNumericCongestionLevels` to ``CongestionLevel`` type + /// (see ``CongestionRange``) or by taking `segmentCongestionLevels`, depending whether + /// `AttributeOptions.numericCongestionLevel` or `AttributeOptions.congestionLevel` was specified in + /// `DirectionsOptions.attributes` during route request. + /// + /// If both are present, `segmentNumericCongestionLevels` is preferred. + /// + /// If none are present, returns `nil`. + public func resolveCongestionLevels(using configuration: CongestionRangesConfiguration) -> [CongestionLevel]? { + let congestionLevels: [CongestionLevel]? = if let numeric = segmentNumericCongestionLevels { + numeric.map { numericValue in + CongestionLevel(numericValue: numericValue, configuration: configuration) + } + } else if let levels = segmentCongestionLevels { + levels + } else { + nil + } + + return congestionLevels + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Coordinate2D.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Coordinate2D.swift new file mode 100644 index 000000000..dd7ac12eb --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Coordinate2D.swift @@ -0,0 +1,15 @@ +import CoreLocation +import Foundation +import MapboxCommon + +extension Coordinate2D { + convenience init(_ coordinate: CLLocationCoordinate2D) { + self.init(value: .init(latitude: coordinate.latitude, longitude: coordinate.longitude)) + } +} + +extension CLLocation { + convenience init(_ coordinate: Coordinate2D) { + self.init(latitude: coordinate.value.latitude, longitude: coordinate.value.longitude) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Date.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Date.swift new file mode 100644 index 000000000..9bca2c195 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Date.swift @@ -0,0 +1,20 @@ +import Foundation + +extension Date { + var ISO8601: String { + return Date.ISO8601Formatter.string(from: self) + } + + static let ISO8601Formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter + }() + + var nanosecondsSince1970: Double { + // UnitDuration.nanoseconds requires iOS 13 + return timeIntervalSince1970 * 1e9 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Dictionary.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Dictionary.swift new file mode 100644 index 000000000..b9b14ca6c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Dictionary.swift @@ -0,0 +1,13 @@ +import Foundation + +extension Dictionary where Value == Any { + mutating func deepMerge(with dictionary: Dictionary, uniquingKeysWith combine: @escaping (Value, Value) -> Value) { + merge(dictionary) { current, new in + guard var currentDict = current as? Dictionary, let newDict = new as? Dictionary else { + return combine(current, new) + } + currentDict.deepMerge(with: newDict, uniquingKeysWith: combine) + return currentDict + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/FixLocation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/FixLocation.swift new file mode 100644 index 000000000..99781d42d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/FixLocation.swift @@ -0,0 +1,39 @@ +import CoreLocation +import Foundation +import MapboxNavigationNative + +extension FixLocation { + convenience init(_ location: CLLocation, isMock: Bool = false) { + let bearingAccuracy = location.courseAccuracy >= 0 ? location.courseAccuracy as NSNumber : nil + + var provider: String? +#if compiler(>=5.5) + if #available(iOS 15.0, *) { + if let sourceInformation = location.sourceInformation { + // in some scenarios we store this information to history files, so to save space there, we use "short" + // names and 1/0 instead of true/false + let isSimulated = sourceInformation.isSimulatedBySoftware ? 1 : 0 + let isProducedByAccessory = sourceInformation.isProducedByAccessory ? 1 : 0 + + provider = "sim:\(isSimulated),acc:\(isProducedByAccessory)" + } + } +#endif + + self.init( + coordinate: location.coordinate, + monotonicTimestampNanoseconds: Int64(location.timestamp.nanosecondsSince1970), + time: location.timestamp, + speed: location.speed >= 0 ? location.speed as NSNumber : nil, + bearing: location.course >= 0 ? location.course as NSNumber : nil, + altitude: location.altitude as NSNumber, + accuracyHorizontal: location.horizontalAccuracy >= 0 ? location.horizontalAccuracy as NSNumber : nil, + provider: provider, + bearingAccuracy: bearingAccuracy, + speedAccuracy: location.speedAccuracy >= 0 ? location.speedAccuracy as NSNumber : nil, + verticalAccuracy: location.verticalAccuracy >= 0 ? location.verticalAccuracy as NSNumber : nil, + extras: [:], + isMock: isMock + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Geometry.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Geometry.swift new file mode 100644 index 000000000..7164722c9 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Geometry.swift @@ -0,0 +1,64 @@ +import Foundation +import MapboxCommon +import MapboxNavigationNative +import Turf + +extension Turf.Geometry { + init(_ native: MapboxCommon.Geometry) { + switch native.geometryType { + case GeometryType_Point: + if let point = native.extractLocations()?.locationValue { + self = .point(Point(point)) + } else { + preconditionFailure("Point can't be constructed. Geometry wasn't extracted.") + } + case GeometryType_Line: + if let coordinates = native.extractLocationsArray()?.map(\.locationValue) { + self = .lineString(LineString(coordinates)) + } else { + preconditionFailure("LineString can't be constructed. Geometry wasn't extracted.") + } + case GeometryType_Polygon: + if let coordinates = native.extractLocations2DArray()?.map({ $0.map(\.locationValue) }) { + self = .polygon(Polygon(coordinates)) + } else { + preconditionFailure("Polygon can't be constructed. Geometry wasn't extracted.") + } + case GeometryType_MultiPoint: + if let coordinates = native.extractLocationsArray()?.map(\.locationValue) { + self = .multiPoint(MultiPoint(coordinates)) + } else { + preconditionFailure("MultiPoint can't be constructed. Geometry wasn't extracted.") + } + case GeometryType_MultiLine: + if let coordinates = native.extractLocations2DArray()?.map({ $0.map(\.locationValue) }) { + self = .multiLineString(MultiLineString(coordinates)) + } else { + preconditionFailure("MultiLineString can't be constructed. Geometry wasn't extracted.") + } + case GeometryType_MultiPolygon: + if let coordinates = native.extractLocations3DArray()?.map({ $0.map { $0.map(\.locationValue) } }) { + self = .multiPolygon(MultiPolygon(coordinates)) + } else { + preconditionFailure("MultiPolygon can't be constructed. Geometry wasn't extracted.") + } + case GeometryType_GeometryCollection: + if let geometries = native.extractGeometriesArray()?.compactMap(Geometry.init) { + self = .geometryCollection(GeometryCollection(geometries: geometries)) + } else { + preconditionFailure("GeometryCollection can't be constructed. Geometry wasn't extracted.") + } + case GeometryType_Empty: + fallthrough + default: + preconditionFailure("Geometry can't be constructed. Unknown type.") + } + } +} + +extension NSValue { + var locationValue: CLLocationCoordinate2D { + let point = cgPointValue + return CLLocationCoordinate2DMake(Double(point.x), Double(point.y)) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Incident.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Incident.swift new file mode 100644 index 000000000..da8380714 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Incident.swift @@ -0,0 +1,80 @@ +import Foundation +import MapboxDirections +import MapboxNavigationNative + +extension Incident { + init(_ incidentInfo: IncidentInfo) { + let incidentType: Incident.Kind + switch incidentInfo.type { + case .accident: + incidentType = .accident + case .congestion: + incidentType = .congestion + case .construction: + incidentType = .construction + case .disabledVehicle: + incidentType = .disabledVehicle + case .laneRestriction: + incidentType = .laneRestriction + case .massTransit: + incidentType = .massTransit + case .miscellaneous: + incidentType = .miscellaneous + case .otherNews: + incidentType = .otherNews + case .plannedEvent: + incidentType = .plannedEvent + case .roadClosure: + incidentType = .roadClosure + case .roadHazard: + incidentType = .roadHazard + case .weather: + incidentType = .weather + @unknown default: + assertionFailure("Unknown IncidentInfo type.") + incidentType = .undefined + } + + self.init( + identifier: incidentInfo.id, + type: incidentType, + description: incidentInfo.description ?? "", + creationDate: incidentInfo.creationTime ?? Date.distantPast, + startDate: incidentInfo.startTime ?? Date.distantPast, + endDate: incidentInfo.endTime ?? Date.distantPast, + impact: .init(incidentInfo.impact), + subtype: incidentInfo.subType, + subtypeDescription: incidentInfo.subTypeDescription, + alertCodes: Set(incidentInfo.alertcCodes.map(\.intValue)), + lanesBlocked: BlockedLanes(descriptions: incidentInfo.lanesBlocked), + shapeIndexRange: -1 ..< -1, + countryCodeAlpha3: incidentInfo.iso_3166_1_alpha3, + countryCode: incidentInfo.iso_3166_1_alpha2, + roadIsClosed: incidentInfo.roadClosed, + longDescription: incidentInfo.longDescription, + numberOfBlockedLanes: incidentInfo.numLanesBlocked?.intValue, + congestionLevel: incidentInfo.congestion?.value?.intValue, + affectedRoadNames: incidentInfo.affectedRoadNames + ) + } +} + +extension Incident.Impact { + init(_ incidentImpact: IncidentImpact) { + switch incidentImpact { + case .unknown: + self = .unknown + case .critical: + self = .critical + case .major: + self = .major + case .minor: + self = .minor + case .low: + self = .low + @unknown default: + assertionFailure("Unknown IncidentImpact value.") + self = .unknown + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Locale.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Locale.swift new file mode 100644 index 000000000..3eb52c7dd --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Locale.swift @@ -0,0 +1,47 @@ +import Foundation + +extension Locale { + /// Given the app's localized language setting, returns a string representing the user's localization. + public static var preferredLocalLanguageCountryCode: String { + let firstBundleLocale = Bundle.main.preferredLocalizations.first! + let bundleLocale = firstBundleLocale.components(separatedBy: "-") + + if bundleLocale.count > 1 { + return firstBundleLocale + } + + if let countryCode = (Locale.current as NSLocale).object(forKey: .countryCode) as? String { + return "\(bundleLocale.first!)-\(countryCode)" + } + + return firstBundleLocale + } + + /// Returns a `Locale` from ``Foundation/Locale/preferredLocalLanguageCountryCode``. + public static var nationalizedCurrent: Locale { + Locale(identifier: preferredLocalLanguageCountryCode) + } + + var BCP47Code: String { + if #available(iOS 16, *) { + language.maximalIdentifier + } else { + languageCode ?? identifier + } + } + + var preferredBCP47Codes: [String] { + let currentCode = BCP47Code + var codes = [currentCode] + for code in Self.preferredLanguages { + let newCode: String = if #available(iOS 16, *) { + Locale(languageCode: Locale.LanguageCode(stringLiteral: code)).BCP47Code + } else { + Locale(identifier: code).BCP47Code + } + guard newCode != currentCode else { continue } + codes.append(newCode) + } + return codes + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MapboxStreetsRoadClass.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MapboxStreetsRoadClass.swift new file mode 100644 index 000000000..bf26c5454 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MapboxStreetsRoadClass.swift @@ -0,0 +1,34 @@ +import Foundation +import MapboxDirections +import MapboxNavigationNative + +extension MapboxStreetsRoadClass { + /// Returns a Boolean value indicating whether the road class is for a highway entrance or exit ramp (slip road). + public var isRamp: Bool { + return self == .motorwayLink || self == .trunkLink || self == .primaryLink || self == .secondaryLink + } + + init(_ native: FunctionalRoadClass, isRamp: Bool) { + switch native { + case .motorway: + self = isRamp ? .motorwayLink : .motorway + case .trunk: + self = isRamp ? .trunkLink : .trunk + case .primary: + self = isRamp ? .primaryLink : .primary + case .secondary: + self = isRamp ? .secondaryLink : .secondary + case .tertiary: + self = isRamp ? .tertiaryLink : .tertiary + case .unclassified, .residential: + // Mapbox Streets conflates unclassified and residential roads, because generally speaking they are + // distinguished only by their abutters; neither is “higher” than the other in priority. + self = .street + case .serviceOther: + self = .service + @unknown default: + assertionFailure("Unknown FunctionalRoadClass value.") + self = .undefined + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MeasurementSystem.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MeasurementSystem.swift new file mode 100644 index 000000000..86ef92a76 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MeasurementSystem.swift @@ -0,0 +1,10 @@ +import Foundation +import MapboxDirections + +extension MeasurementSystem { + /// Converts `LengthFormatter.Unit` into `MapboxDirections.MeasurementSystem`. + public init(_ lengthUnit: LengthFormatter.Unit) { + let metricUnits: [LengthFormatter.Unit] = [.kilometer, .centimeter, .meter, .millimeter] + self = metricUnits.contains(lengthUnit) ? .metric : .imperial + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/NavigationStatus.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/NavigationStatus.swift new file mode 100644 index 000000000..7b2fafafb --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/NavigationStatus.swift @@ -0,0 +1,36 @@ +import Foundation +import MapboxDirections +import MapboxNavigationNative + +extension NavigationStatus { + private static let nameSeparator = " / " + + func localizedRoadName(locale: Locale = .nationalizedCurrent) -> RoadName { + let roadNames = localizedRoadNames(locale: locale) + + let name = roadNames.first { $0.shield == nil } ?? nonLocalizedRoadName + let shield = localizedShield(locale: locale).map(RoadShield.init) + return .init(text: name.text, language: name.language, shield: shield) + } + + private var nonLocalizedRoadName: MapboxNavigationNative.RoadName { + let text = roads + .filter { $0.shield == nil } + .map(\.text) + .joined(separator: NavigationStatus.nameSeparator) + return .init(text: text, language: "", imageBaseUrl: nil, shield: nil) + } + + private func localizedShield(locale: Locale) -> Shield? { + let roadNames = localizedRoadNames(locale: locale) + return roadNames.compactMap(\.shield).first ?? shield + } + + private func localizedRoadNames(locale: Locale) -> [MapboxNavigationNative.RoadName] { + roads.filter { $0.language == locale.languageCode } + } + + private var shield: Shield? { + roads.compactMap(\.shield).first + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Preconcurrency+Sendable.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Preconcurrency+Sendable.swift new file mode 100644 index 000000000..9f493bf5c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Preconcurrency+Sendable.swift @@ -0,0 +1,7 @@ +import MapboxMaps +import Turf + +extension LineString: @unchecked Sendable {} + +extension Puck3DConfiguration: @unchecked Sendable {} +extension Puck2DConfiguration: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RestStop.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RestStop.swift new file mode 100644 index 000000000..49263a25d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RestStop.swift @@ -0,0 +1,25 @@ +import Foundation +import MapboxDirections +import MapboxNavigationNative + +extension RestStop { + init?(_ serviceArea: ServiceAreaInfo) { + let amenities: [MapboxDirections.Amenity] = serviceArea.amenities.map { amenity in + Amenity( + type: AmenityType(amenity.type), + name: amenity.name, + brand: amenity.brand + ) + } + + switch serviceArea.type { + case .restArea: + self.init(type: .restArea, name: serviceArea.name, amenities: amenities) + case .serviceArea: + self.init(type: .serviceArea, name: serviceArea.name, amenities: amenities) + @unknown default: + assertionFailure("Unknown ServiceAreaInfo type.") + return nil + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Result.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Result.swift new file mode 100644 index 000000000..753f38e60 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Result.swift @@ -0,0 +1,20 @@ +import Foundation +import MapboxCommon_Private + +extension Result { + init(expected: Expected) { + if expected.isValue(), let value = expected.value { + guard let typedValue = value as? Success else { + preconditionFailure("Result value can't be constructed. Unknown expected value type.") + } + self = .success(typedValue) + } else if expected.isError(), let error = expected.error { + guard let typedError = error as? Failure else { + preconditionFailure("Result error can't be constructed. Unknown expected error type.") + } + self = .failure(typedError) + } else { + preconditionFailure("Expected type is neither a value nor an error.") + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteLeg.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteLeg.swift new file mode 100644 index 000000000..cc0af2297 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteLeg.swift @@ -0,0 +1,46 @@ +import MapboxDirections +import Turf + +extension RouteLeg { + public var shape: LineString { + return steps.dropFirst().reduce(into: steps.first?.shape ?? LineString([])) { result, step in + result.coordinates += (step.shape?.coordinates ?? []).dropFirst() + } + } + + func mapIntersectionsAttributes(_ attributeTransform: (Intersection) -> T) -> [T] { + // Pick only valid segment indices for specific `Intersection` in `RouteStep`. + // Array of segment indexes can look like this: [0, 3, 24, 28, 48, 50, 51, 53]. + let segmentIndices = steps.compactMap { $0.segmentIndicesByIntersection?.compactMap { $0 } }.reduce([], +) + + // Pick selected attribute by `attributeTransform` in specific `Intersection` of `RouteStep`. + // It is expected that number of `segmentIndices` will be equal to number of `attributesInLeg`. + // Array may include optionals and nil values. + let attributesInLeg = steps.compactMap { $0.intersections?.map(attributeTransform) }.reduce([], +) + + // Map each selected attribute to the amount of two adjacent `segmentIndices`. + // At the end amount of attributes should be equal to the last segment index. + let streetsRoadClasses = segmentIndices.enumerated().map { + segmentIndices.indices.contains($0.offset + 1) && attributesInLeg.indices.contains($0.offset) ? + Array( + repeating: attributesInLeg[$0.offset], + count: segmentIndices[$0.offset + 1] - segmentIndices[$0.offset] + ) : [] + + }.reduce([], +) + + return streetsRoadClasses + } + + /// Returns an array of `MapboxStreetsRoadClass` objects for specific leg. `MapboxStreetsRoadClass` will be set to + /// `nil` if it's not present in `Intersection`. + public var streetsRoadClasses: [MapboxStreetsRoadClass?] { + return mapIntersectionsAttributes { $0.outletMapboxStreetsRoadClass } + } + + /// Returns an array of `RoadClasses` objects for specific leg. `RoadClasses` will be set to `nil` if it's not + /// present in `Intersection`. + public var roadClasses: [RoadClasses?] { + return mapIntersectionsAttributes { $0.outletRoadClasses } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteOptions.swift new file mode 100644 index 000000000..854be7e29 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteOptions.swift @@ -0,0 +1,62 @@ +import CoreLocation +import MapboxDirections + +extension RouteOptions { + var activityType: CLActivityType { + switch profileIdentifier { + case .cycling, .walking: + return .fitness + default: + return .otherNavigation + } + } + + /// Returns a tuple containing the waypoints along the leg at the given index and the waypoints that separate + /// subsequent legs. + /// + /// The first element of the tuple includes the leg’s source but not its destination. + func waypoints(fromLegAt legIndex: Int) -> ([Waypoint], [Waypoint]) { + // The first and last waypoints always separate legs. Make exceptions for these waypoints instead of modifying + // them by side effect. + let legSeparators = waypoints.filterKeepingFirstAndLast { $0.separatesLegs } + let viaPointsByLeg = waypoints.splitExceptAtStartAndEnd(omittingEmptySubsequences: false) { $0.separatesLegs } + .dropFirst() // No leg precedes first separator. + + let reconstitutedWaypoints = zip(legSeparators, viaPointsByLeg).dropFirst(legIndex).map { [$0.0] + $0.1 } + let legWaypoints = reconstitutedWaypoints.first ?? [] + let subsequentWaypoints = reconstitutedWaypoints.dropFirst() + return (legWaypoints, subsequentWaypoints.flatMap { $0 }) + } +} + +extension RouteOptions { + /// Returns a copy of the route options by roundtripping through JSON. + /// + /// - Throws: An `EncodingError` or `DecodingError` if the route options could not be roundtripped through JSON. + func copy() throws -> Self { + // TODO: remove this method when changed to value type. + // Work around . + let encodedOptions = try JSONEncoder().encode(self) + return try JSONDecoder().decode(type(of: self), from: encodedOptions) + } +} + +extension Array { + /// - seealso: `Array.filter(_:)` + public func filterKeepingFirstAndLast(_ isIncluded: (Element) throws -> Bool) rethrows -> [Element] { + return try enumerated().filter { + try isIncluded($0.element) || $0.offset == 0 || $0.offset == indices.last + }.map(\.element) + } + + /// - seealso: `Array.split(maxSplits:omittingEmptySubsequences:whereSeparator:)` + public func splitExceptAtStartAndEnd( + maxSplits: Int = .max, + omittingEmptySubsequences: Bool = true, + whereSeparator isSeparator: (Element) throws -> Bool + ) rethrows -> [ArraySlice] { + return try enumerated().split(maxSplits: maxSplits, omittingEmptySubsequences: omittingEmptySubsequences) { + try isSeparator($0.element) || $0.offset == 0 || $0.offset == indices.last + }.map { $0.map(\.element).suffix(from: 0) } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/SpokenInstruction.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/SpokenInstruction.swift new file mode 100644 index 000000000..022c17ff7 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/SpokenInstruction.swift @@ -0,0 +1,52 @@ +import AVFoundation +import Foundation +import MapboxDirections + +extension SpokenInstruction { + func attributedText(for legProgress: RouteLegProgress) -> NSAttributedString { + let attributedText = NSMutableAttributedString(string: text) + if let step = legProgress.upcomingStep, + let name = step.names?.first, + let phoneticName = step.phoneticNames?.first + { + let nameRange = attributedText.mutableString.range(of: name) + if nameRange.location != NSNotFound { + attributedText.replaceCharacters( + in: nameRange, + with: NSAttributedString(string: name).pronounced(phoneticName) + ) + } + } + if let step = legProgress.followOnStep, + let name = step.names?.first, + let phoneticName = step.phoneticNames?.first + { + let nameRange = attributedText.mutableString.range(of: name) + if nameRange.location != NSNotFound { + attributedText.replaceCharacters( + in: nameRange, + with: NSAttributedString(string: name).pronounced(phoneticName) + ) + } + } + return attributedText + } +} + +extension NSAttributedString { + public func pronounced(_ pronunciation: String) -> NSAttributedString { + let phoneticWords = pronunciation.components(separatedBy: " ") + let phoneticString = NSMutableAttributedString() + for (word, phoneticWord) in zip(string.components(separatedBy: " "), phoneticWords) { + // AVSpeechSynthesizer doesn’t recognize some common IPA symbols. + let phoneticWord = phoneticWord.byReplacing([("ɡ", "g"), ("ɹ", "r")]) + if phoneticString.length > 0 { + phoneticString.append(NSAttributedString(string: " ")) + } + phoneticString.append(NSAttributedString(string: word, attributes: [ + NSAttributedString.Key(rawValue: AVSpeechSynthesisIPANotationAttribute): phoneticWord, + ])) + } + return phoneticString + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/String.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/String.swift new file mode 100644 index 000000000..3e293ca58 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/String.swift @@ -0,0 +1,58 @@ +import CommonCrypto +import Foundation + +extension String { + typealias Replacement = (of: String, with: String) + + func byReplacing(_ replacements: [Replacement]) -> String { + return replacements.reduce(self) { $0.replacingOccurrences(of: $1.of, with: $1.with) } + } + + /// Returns the SHA256 hash of the string. + var sha256: String { + let length = Int(CC_SHA256_DIGEST_LENGTH) + let digest = utf8CString.withUnsafeBufferPointer { body -> [UInt8] in + var digest = [UInt8](repeating: 0, count: length) + CC_SHA256(body.baseAddress, CC_LONG(lengthOfBytes(using: .utf8)), &digest) + return digest + } + return digest.lazy.map { String(format: "%02x", $0) }.joined() + } + + // Adapted from https://github.com/raywenderlich/swift-algorithm-club/blob/master/Minimum%20Edit%20Distance/MinimumEditDistance.playground/Contents.swift + public func minimumEditDistance(to word: String) -> Int { + let fromWordCount = count + let toWordCount = word.count + + guard !isEmpty else { return toWordCount } + guard !word.isEmpty else { return fromWordCount } + + var matrix = [[Int]](repeating: [Int](repeating: 0, count: toWordCount + 1), count: fromWordCount + 1) + + // initialize matrix + for index in 1...fromWordCount { + // the distance of any first string to an empty second string + matrix[index][0] = index + } + + for index in 1...toWordCount { + // the distance of any second string to an empty first string + matrix[0][index] = index + } + + // compute Levenshtein distance + for (i, selfChar) in enumerated() { + for (j, otherChar) in word.enumerated() { + if otherChar == selfChar { + // substitution of equal symbols with cost 0 + matrix[i + 1][j + 1] = matrix[i][j] + } else { + // minimum of the cost of insertion, deletion, or substitution + // added to the already computed costs in the corresponding cells + matrix[i + 1][j + 1] = Swift.min(matrix[i][j] + 1, matrix[i + 1][j] + 1, matrix[i][j + 1] + 1) + } + } + } + return matrix[fromWordCount][toWordCount] + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/TollCollection.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/TollCollection.swift new file mode 100644 index 000000000..7aad46a8e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/TollCollection.swift @@ -0,0 +1,18 @@ + +import Foundation +import MapboxDirections +import MapboxNavigationNative + +extension TollCollection { + init?(_ tollInfo: TollCollectionInfo) { + switch tollInfo.type { + case .tollBooth: + self.init(type: .booth, name: tollInfo.name) + case .tollGantry: + self.init(type: .gantry, name: tollInfo.name) + @unknown default: + assertionFailure("Unknown TollCollectionInfo type.") + return nil + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIDevice.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIDevice.swift new file mode 100644 index 000000000..625f2d6e1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIDevice.swift @@ -0,0 +1,22 @@ +import UIKit + +extension UIDevice { + static var isSimulator: Bool { +#if targetEnvironment(simulator) + return true +#else + return false +#endif + } + + var screenOrientation: UIDeviceOrientation { + let screenOrientation: UIDeviceOrientation = if orientation.isValidInterfaceOrientation { + orientation + } else if UIScreen.main.bounds.height > UIScreen.main.bounds.width { + .portrait + } else { + .landscapeLeft + } + return screenOrientation + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIEdgeInsets.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIEdgeInsets.swift new file mode 100644 index 000000000..8ae0b7e2c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIEdgeInsets.swift @@ -0,0 +1,33 @@ +import Foundation +import UIKit + +extension UIEdgeInsets { + static func + (left: UIEdgeInsets, right: UIEdgeInsets) -> UIEdgeInsets { + return UIEdgeInsets( + top: left.top + right.top, + left: left.left + right.left, + bottom: left.bottom + right.bottom, + right: left.right + right.right + ) + } + + static func += (lhs: inout UIEdgeInsets, rhs: UIEdgeInsets) { + lhs.top += rhs.top + lhs.left += rhs.left + lhs.bottom += rhs.bottom + lhs.right += rhs.right + } + + func rectValue(_ rect: CGRect) -> CGRect { + return CGRect( + x: rect.origin.x + left, + y: rect.origin.y + top, + width: rect.size.width - left - right, + height: rect.size.height - top - bottom + ) + } + + static var centerEdgeInsets: UIEdgeInsets { + return UIEdgeInsets(top: 10.0, left: 20.0, bottom: 10.0, right: 20.0) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Utils.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Utils.swift new file mode 100644 index 000000000..5b21ab506 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Utils.swift @@ -0,0 +1,57 @@ +import CoreGraphics +import CoreLocation +import Foundation + +private let tileSize: Double = 512.0 +private let M2PI = Double.pi * 2 +private let MPI2 = Double.pi / 2 +private let DEG2RAD = Double.pi / 180.0 +private let EARTH_RADIUS_M = 6378137.0 +private let LATITUDE_MAX: Double = 85.051128779806604 +private let AngularFieldOfView: CLLocationDegrees = 30 +private let MIN_ZOOM = 0.0 +private let MAX_ZOOM = 25.5 + +func AltitudeForZoomLevel( + _ zoomLevel: Double, + _ pitch: CGFloat, + _ latitude: CLLocationDegrees, + _ size: CGSize +) -> CLLocationDistance { + let metersPerPixel = getMetersPerPixelAtLatitude(latitude, zoomLevel) + let metersTall = metersPerPixel * Double(size.height) + let altitude = metersTall / 2 / tan(RadiansFromDegrees(AngularFieldOfView) / 2) + return altitude * sin(MPI2 - RadiansFromDegrees(CLLocationDegrees(pitch))) / sin(MPI2) +} + +func ZoomLevelForAltitude( + _ altitude: CLLocationDistance, + _ pitch: CGFloat, + _ latitude: CLLocationDegrees, + _ size: CGSize +) -> Double { + let eyeAltitude = altitude / sin(MPI2 - RadiansFromDegrees(CLLocationDegrees(pitch))) * sin(MPI2) + let metersTall = eyeAltitude * 2 * tan(RadiansFromDegrees(AngularFieldOfView) / 2) + let metersPerPixel = metersTall / Double(size.height) + let mapPixelWidthAtZoom = cos(RadiansFromDegrees(latitude)) * M2PI * EARTH_RADIUS_M / metersPerPixel + return log2(mapPixelWidthAtZoom / tileSize) +} + +private func clamp(_ value: Double, _ min: Double, _ max: Double) -> Double { + return fmax(min, fmin(max, value)) +} + +private func worldSize(_ scale: Double) -> Double { + return scale * tileSize +} + +private func RadiansFromDegrees(_ degrees: CLLocationDegrees) -> Double { + return degrees * Double.pi / 180 +} + +func getMetersPerPixelAtLatitude(_ lat: Double, _ zoom: Double) -> Double { + let constrainedZoom = clamp(zoom, MIN_ZOOM, MAX_ZOOM) + let constrainedScale = pow(2.0, constrainedZoom) + let constrainedLatitude = clamp(lat, -LATITUDE_MAX, LATITUDE_MAX) + return cos(constrainedLatitude * DEG2RAD) * M2PI * EARTH_RADIUS_M / worldSize(constrainedScale) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/ActiveNavigationFeedbackType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/ActiveNavigationFeedbackType.swift new file mode 100644 index 000000000..e9c9708f8 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/ActiveNavigationFeedbackType.swift @@ -0,0 +1,79 @@ +import Foundation + +/// Feedback type is used to specify the type of feedback being recorded with +/// ``NavigationEventsManager/sendActiveNavigationFeedback(_:type:description:)``. +public enum ActiveNavigationFeedbackType: FeedbackType { + case closure + case poorRoute + case wrongSpeedLimit + + case badRoute + case illegalTurn + case roadClosed + case incorrectLaneGuidance + case other + case arrival(rating: Int) + + case falsePositiveTraffic + case falseNegativeTraffic + case missingConstruction + case missingSpeedLimit + + /// Description of the category for this type of feedback + public var typeKey: String { + switch self { + case .closure: + return "ag_missing_closure" + case .poorRoute: + return "ag_poor_route" + case .wrongSpeedLimit: + return "ag_wrong_speed_limit" + case .badRoute: + return "routing_error" + case .illegalTurn: + return "turn_was_not_allowed" + case .roadClosed: + return "road_closed" + case .incorrectLaneGuidance: + return "lane_guidance_incorrect" + case .other: + return "other_navigation" + case .arrival: + return "arrival" + case .falsePositiveTraffic: + return "ag_fp_traffic" + case .falseNegativeTraffic: + return "ag_fn_traffic" + case .missingConstruction: + return "ag_missing_construction" + case .missingSpeedLimit: + return "ag_missing_speed_limit" + } + } + + /// Optional detailed description of the subtype of this feedback + public var subtypeKey: String? { + if case .arrival(let rating) = self { + return String(rating) + } + return nil + } +} + +/// Enum denoting the origin source of the corresponding feedback item +public enum FeedbackSource: Int, CustomStringConvertible, Sendable { + case user + case reroute + case unknown + + public var description: String { + switch self { + case .user: + return "user" + case .reroute: + return "reroute" + case .unknown: + return "unknown" + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventFixLocation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventFixLocation.swift new file mode 100644 index 000000000..9221e7a6e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventFixLocation.swift @@ -0,0 +1,119 @@ +import CoreLocation +import Foundation +import MapboxNavigationNative + +struct EventFixLocation { + let coordinate: CLLocationCoordinate2D + let altitude: CLLocationDistance? + let time: Date + let monotonicTimestampNanoseconds: Int64 + let horizontalAccuracy: CLLocationAccuracy? + let verticalAccuracy: CLLocationAccuracy? + let bearingAccuracy: CLLocationDirectionAccuracy? + let speedAccuracy: CLLocationSpeedAccuracy? + let bearing: CLLocationDirection? + let speed: CLLocationSpeed? + let provider: String? + let extras: [String: Any] + let isMock: Bool + + /// Initializes an event location consistent with the given location object. + init(_ location: FixLocation) { + self.coordinate = location.coordinate + self.altitude = location.altitude?.doubleValue + self.time = location.time + self.monotonicTimestampNanoseconds = location.monotonicTimestampNanoseconds + self.speed = location.speed?.doubleValue + self.bearing = location.bearing?.doubleValue + self.bearingAccuracy = location.bearingAccuracy?.doubleValue + self.horizontalAccuracy = location.accuracyHorizontal?.doubleValue + self.verticalAccuracy = location.verticalAccuracy?.doubleValue + self.speedAccuracy = location.speedAccuracy?.doubleValue + self.provider = location.provider + self.extras = location.extras + self.isMock = location.isMock + } +} + +extension EventFixLocation: Codable { + private enum CodingKeys: String, CodingKey { + case latitude = "lat" + case longitude = "lon" + case monotonicTimestampNanoseconds + case time + case speed + case bearing + case altitude + case accuracyHorizontal + case provider + case bearingAccuracy + case speedAccuracy + case verticalAccuracy + case extras + case isMock + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let latitude = try container.decode(CLLocationDegrees.self, forKey: .latitude) + let longitude = try container.decode(CLLocationDegrees.self, forKey: .longitude) + let extrasData = try container.decode(Data.self, forKey: .extras) + + let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + let fixLocation = try FixLocation( + coordinate: coordinate, + monotonicTimestampNanoseconds: container.decode(Int64.self, forKey: .monotonicTimestampNanoseconds), + time: container.decode(Date.self, forKey: .time), + speed: container.decodeIfPresent(Double.self, forKey: .speed) as NSNumber?, + bearing: container.decodeIfPresent(Double.self, forKey: .bearing) as NSNumber?, + altitude: container.decodeIfPresent(Double.self, forKey: .altitude) as NSNumber?, + accuracyHorizontal: container.decodeIfPresent(Double.self, forKey: .accuracyHorizontal) as NSNumber?, + provider: container.decodeIfPresent(String.self, forKey: .provider), + bearingAccuracy: container.decodeIfPresent(Double.self, forKey: .bearingAccuracy) as NSNumber?, + speedAccuracy: container.decodeIfPresent(Double.self, forKey: .speedAccuracy) as NSNumber?, + verticalAccuracy: container.decodeIfPresent(Double.self, forKey: .verticalAccuracy) as NSNumber?, + extras: JSONSerialization.jsonObject(with: extrasData) as? [String: Any] ?? [:], + isMock: container.decode(Bool.self, forKey: .isMock) + ) + self.init(fixLocation) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(coordinate.latitude, forKey: .latitude) + try container.encode(coordinate.longitude, forKey: .longitude) + try container.encode(monotonicTimestampNanoseconds, forKey: .monotonicTimestampNanoseconds) + try container.encode(time, forKey: .time) + try container.encode(isMock, forKey: .isMock) + try container.encodeIfPresent(speed, forKey: .speed) + try container.encodeIfPresent(bearing, forKey: .bearing) + try container.encodeIfPresent(altitude, forKey: .altitude) + try container.encodeIfPresent(provider, forKey: .provider) + try container.encodeIfPresent(bearingAccuracy, forKey: .bearingAccuracy) + try container.encodeIfPresent(speedAccuracy, forKey: .speedAccuracy) + try container.encodeIfPresent(verticalAccuracy, forKey: .verticalAccuracy) + let extrasData = try JSONSerialization.data(withJSONObject: extras) + try container.encode(extrasData, forKey: .extras) + } +} + +extension FixLocation { + convenience init(_ location: EventFixLocation) { + self.init( + coordinate: location.coordinate, + monotonicTimestampNanoseconds: location.monotonicTimestampNanoseconds, + time: location.time, + speed: location.speed as NSNumber?, + bearing: location.bearing as NSNumber?, + altitude: location.altitude as NSNumber?, + accuracyHorizontal: location.horizontalAccuracy as NSNumber?, + provider: location.provider, + bearingAccuracy: location.bearingAccuracy as NSNumber?, + speedAccuracy: location.speedAccuracy as NSNumber?, + verticalAccuracy: location.verticalAccuracy as NSNumber?, + extras: location.extras, + isMock: location.isMock + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventStep.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventStep.swift new file mode 100644 index 000000000..d5b1f3454 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventStep.swift @@ -0,0 +1,55 @@ +import CoreLocation +import Foundation +import MapboxNavigationNative + +struct EventStep: Equatable, Codable { + let distance: Double + let distanceRemaining: Double + let duration: Double + let durationRemaining: Double + let upcomingName: String + let upcomingType: String + let upcomingModifier: String + let upcomingInstruction: String + let previousName: String + let previousType: String + let previousModifier: String + let previousInstruction: String + + /// Initializes an event location consistent with the given location object. + init(_ step: Step) { + self.distance = step.distance + self.distanceRemaining = step.distanceRemaining + self.duration = step.duration + self.durationRemaining = step.durationRemaining + self.upcomingName = step.upcomingName + self.upcomingType = step.upcomingType + self.upcomingModifier = step.upcomingModifier + self.upcomingInstruction = step.upcomingInstruction + self.previousName = step.previousName + self.previousType = step.previousType + self.previousModifier = step.previousModifier + self.previousInstruction = step.previousInstruction + } +} + +extension Step { + convenience init?(_ step: EventStep?) { + guard let step else { return nil } + + self.init( + distance: step.distance, + distanceRemaining: step.distanceRemaining, + duration: step.duration, + durationRemaining: step.durationRemaining, + upcomingName: step.upcomingName, + upcomingType: step.upcomingType, + upcomingModifier: step.upcomingModifier, + upcomingInstruction: step.upcomingInstruction, + previousName: step.previousName, + previousType: step.previousType, + previousModifier: step.previousModifier, + previousInstruction: step.previousInstruction + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventsManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventsManager.swift new file mode 100644 index 000000000..a6d926571 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventsManager.swift @@ -0,0 +1,177 @@ +import CoreLocation +import Foundation +import MapboxCommon +import MapboxNavigationNative_Private +import UIKit + +/// The ``NavigationEventsManager`` is responsible for being the liaison between MapboxCoreNavigation and the Mapbox +/// telemetry. +public final class NavigationEventsManager: Sendable { + let navNativeEventsManager: NavigationTelemetryManager? + + // MARK: Configuring Events + + /// Optional application metadata that that can help Mapbox more reliably diagnose problems that occur in the SDK. + /// For example, you can provide your application’s name and version, a unique identifier for the end user, and a + /// session identifier. + /// To include this information, use the following keys: "name", "version", "userId", and "sessionId". + public var userInfo: [String: String?]? { + get { navNativeEventsManager?.userInfo } + set { navNativeEventsManager?.userInfo = newValue } + } + + required init( + eventsMetadataProvider: EventsMetadataProvider, + telemetry: Telemetry + ) { + self.navNativeEventsManager = NavigationNativeEventsManager( + eventsMetadataProvider: eventsMetadataProvider, + telemetry: telemetry + ) + } + + init(navNativeEventsManager: NavigationTelemetryManager?) { + self.navNativeEventsManager = navNativeEventsManager + } + + // MARK: Sending Feedback Events + + /// Create feedback about the current road segment/maneuver to be sent to the Mapbox data team. + /// + /// You can pair this with a custom feedback UI in your app to flag problems during navigation such as road + /// closures, incorrect instructions, etc. + /// If you provide a custom feedback UI that lets users elaborate on an issue, you should call this before you show + /// the custom UI so the location and timestamp are more accurate. Alternatively, you can use + /// `FeedbackViewContoller` which handles feedback lifecycle internally. + /// - Parameter screenshotOption: The options to configure how the screenshot for the vent is provided. + /// - Returns: A ``FeedbackEvent``. + /// - Postcondition: Call ``sendActiveNavigationFeedback(_:type:description:)`` and + /// ``sendPassiveNavigationFeedback(_:type:description:)`` with the returned feedback to attach additional metadata + /// to the feedback and send it. + public func createFeedback(screenshotOption: FeedbackScreenshotOption = .automatic) async -> FeedbackEvent? { + await navNativeEventsManager?.createFeedback(screenshotOption: screenshotOption) + } + + /// You can pair this with a custom feedback UI in your app to flag problems during navigation such as road + /// closures, incorrect instructions, etc. + /// - Parameters: + /// - feedback: A ``FeedbackEvent`` created with ``createFeedback(screenshotOption:)`` method. + /// - type: An ``ActiveNavigationFeedbackType`` used to specify the type of feedback. + /// - description: A custom string used to describe the problem in detail. + public func sendActiveNavigationFeedback( + _ feedback: FeedbackEvent, + type: ActiveNavigationFeedbackType, + description: String? = nil + ) { + Task { + await sendActiveNavigationFeedback( + feedback, + type: type, + description: description, + source: .user + ) + } + } + + /// Send passive navigation feedback to the Mapbox data team. + /// + /// You can pair this with a custom feedback UI in your app to flag problems during navigation such as road + /// closures, incorrect instructions, etc. + /// - Parameters: + /// - feedback: A ``FeedbackEvent`` created with ``createFeedback(screenshotOption:)`` method. + /// - type: A ``PassiveNavigationFeedbackType`` used to specify the type of feedback. + /// - description: A custom string used to describe the problem in detail. + public func sendPassiveNavigationFeedback( + _ feedback: FeedbackEvent, + type: PassiveNavigationFeedbackType, + description: String? = nil + ) { + Task { + await sendPassiveNavigationFeedback( + feedback, + type: type, + description: description, + source: .user + ) + } + } + + /// Send active navigation feedback to the Mapbox data team. + /// + /// You can pair this with a custom feedback UI in your app to flag problems during navigation such as road + /// closures, incorrect instructions, etc. + /// - Parameters: + /// - feedback: A ``FeedbackEvent`` created with ``createFeedback(screenshotOption:)`` method. + /// - type: An ``ActiveNavigationFeedbackType`` used to specify the type of feedback. + /// - description: A custom string used to describe the problem in detail. + /// - source: A ``FeedbackSource`` used to specify feedback source. + /// - Returns: The sent ``UserFeedback``. + public func sendActiveNavigationFeedback( + _ feedback: FeedbackEvent, + type: ActiveNavigationFeedbackType, + description: String?, + source: FeedbackSource + ) async -> UserFeedback? { + return try? await navNativeEventsManager?.sendActiveNavigationFeedback( + feedback, + type: type, + description: description, + source: source + ) + } + + /// Send navigation feedback to the Mapbox data team. + /// - Parameters: + /// - feedback: A ``FeedbackEvent`` created with ``createFeedback(screenshotOption:)`` method. + /// - type: An ``FeedbackType`` used to specify the type of feedback. + /// - description: A custom string used to describe the problem in detail. + /// - source: A ``FeedbackSource`` used to specify feedback source. + /// - Returns: The sent ``UserFeedback``. + public func sendNavigationFeedback( + _ feedback: FeedbackEvent, + type: FeedbackType, + description: String?, + source: FeedbackSource + ) async throws -> UserFeedback? { + return try? await navNativeEventsManager?.sendNavigationFeedback( + feedback, + type: type, + description: description, + source: source + ) + } + + /// Send passive navigation feedback to the Mapbox data team. + /// + /// You can pair this with a custom feedback UI in your app to flag problems during navigation such as road + /// closures, incorrect instructions, etc. + /// - Parameters: + /// - feedback: A ``FeedbackEvent`` created with ``createFeedback(screenshotOption:)`` method. + /// - type: A ``PassiveNavigationFeedbackType`` used to specify the type of feedback. + /// - description: A custom string used to describe the problem in detail. + /// - source: A `FeedbackSource` used to specify feedback source. + /// - Returns: The sent ``UserFeedback``. + public func sendPassiveNavigationFeedback( + _ feedback: FeedbackEvent, + type: PassiveNavigationFeedbackType, + description: String?, + source: FeedbackSource + ) async -> UserFeedback? { + return try? await navNativeEventsManager?.sendPassiveNavigationFeedback( + feedback, + type: type, + description: description, + source: source + ) + } + + /// Send event that Car Play was connected. + public func sendCarPlayConnectEvent() { + navNativeEventsManager?.sendCarPlayConnectEvent() + } + + /// Send event that Car Play was disconnected. + public func sendCarPlayDisconnectEvent() { + navNativeEventsManager?.sendCarPlayDisconnectEvent() + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackEvent.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackEvent.swift new file mode 100644 index 000000000..312371e51 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackEvent.swift @@ -0,0 +1,16 @@ +import Foundation + +/// Feedback event that can be created using ``NavigationEventsManager/createFeedback(screenshotOption:)``. +/// Use ``NavigationEventsManager/sendActiveNavigationFeedback(_:type:description:)`` to send it to the server. +/// Conforms to the `Codable` protocol, so the application can store the event persistently. +public struct FeedbackEvent: Codable, Equatable, Sendable { + public let metadata: FeedbackMetadata + + init(metadata: FeedbackMetadata) { + self.metadata = metadata + } + + public var contents: [String: Any] { + return metadata.contents + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackMetadata.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackMetadata.swift new file mode 100644 index 000000000..dc03696e4 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackMetadata.swift @@ -0,0 +1,103 @@ +import CoreLocation +import Foundation +import MapboxNavigationNative +import MapboxNavigationNative_Private + +public struct FeedbackMetadata: Sendable, Equatable { + public static func == (lhs: FeedbackMetadata, rhs: FeedbackMetadata) -> Bool { + let handlesAreEqual: Bool = switch (lhs.userFeedbackHandle, rhs.userFeedbackHandle) { + case (let lhsHandle as UserFeedbackHandle, let rhsHandle as UserFeedbackHandle): + lhsHandle == rhsHandle + default: + true + } + return handlesAreEqual && + lhs.calculatedUserFeedbackMetadata == rhs.calculatedUserFeedbackMetadata && + lhs.screenshot == rhs.screenshot + } + + private let userFeedbackHandle: (any NativeUserFeedbackHandle)? + private let calculatedUserFeedbackMetadata: UserFeedbackMetadata? + + var userFeedbackMetadata: UserFeedbackMetadata? { + calculatedUserFeedbackMetadata ?? userFeedbackHandle?.getMetadata() + } + + public let screenshot: String? + public var contents: [String: Any] { + guard let data = try? JSONEncoder().encode(self), + let dictionary = try? JSONSerialization.jsonObject( + with: data, options: .allowFragments + ) as? [String: Any] + else { + Log.warning("Unable to encode feedback event details", category: .navigation) + return [:] + } + return dictionary + } + + init( + userFeedbackHandle: (any NativeUserFeedbackHandle)?, + screenshot: String?, + userFeedbackMetadata: UserFeedbackMetadata? = nil + ) { + self.userFeedbackHandle = userFeedbackHandle + self.screenshot = screenshot + self.calculatedUserFeedbackMetadata = userFeedbackMetadata + } +} + +extension FeedbackMetadata: Codable { + fileprivate enum CodingKeys: String, CodingKey { + case screenshot + case locationsBefore + case locationsAfter + case step + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.screenshot = try container.decodeIfPresent(String.self, forKey: .screenshot) + self.calculatedUserFeedbackMetadata = try? UserFeedbackMetadata(from: decoder) + self.userFeedbackHandle = nil + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(screenshot, forKey: .screenshot) + try userFeedbackMetadata?.encode(to: encoder) + } +} + +protocol NativeUserFeedbackHandle: Sendable { + func getMetadata() -> UserFeedbackMetadata +} + +extension UserFeedbackHandle: NativeUserFeedbackHandle, @unchecked Sendable {} + +extension UserFeedbackMetadata: @unchecked Sendable {} + +extension UserFeedbackMetadata: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: FeedbackMetadata.CodingKeys.self) + let eventLocationsAfter: [EventFixLocation] = locationsAfter.map { .init($0) } + let eventLocationsBefore: [EventFixLocation] = locationsBefore.map { .init($0) } + let eventStep = step.map { EventStep($0) } + try container.encode(eventLocationsAfter, forKey: .locationsAfter) + try container.encode(eventLocationsBefore, forKey: .locationsBefore) + try container.encodeIfPresent(eventStep, forKey: .step) + } + + convenience init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: FeedbackMetadata.CodingKeys.self) + let locationsBefore = try container.decode([EventFixLocation].self, forKey: .locationsBefore) + let locationsAfter = try container.decode([EventFixLocation].self, forKey: .locationsAfter) + let eventStep = try container.decodeIfPresent(EventStep.self, forKey: .step) + + self.init( + locationsBefore: locationsBefore.map { FixLocation($0) }, + locationsAfter: locationsAfter.map { FixLocation($0) }, + step: Step(eventStep) + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackScreenshotOption.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackScreenshotOption.swift new file mode 100644 index 000000000..203022b95 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackScreenshotOption.swift @@ -0,0 +1,7 @@ +import UIKit + +/// Indicates screenshotting behavior of ``NavigationEventsManager``. +public enum FeedbackScreenshotOption: Sendable { + case automatic + case custom(UIImage) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackType.swift new file mode 100644 index 000000000..fa5518d3a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackType.swift @@ -0,0 +1,7 @@ +import Foundation + +/// Common protocol for ``ActiveNavigationFeedbackType`` and ``PassiveNavigationFeedbackType``. +public protocol FeedbackType: Sendable { + var typeKey: String { get } + var subtypeKey: String? { get } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/NavigationEventsManagerError.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/NavigationEventsManagerError.swift new file mode 100644 index 000000000..927a070c5 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/NavigationEventsManagerError.swift @@ -0,0 +1,8 @@ +import Foundation + +/// An error that occures during event sending. +@_spi(MapboxInternal) +public enum NavigationEventsManagerError: LocalizedError { + case failedToSend(reason: String) + case invalidData +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/PassiveNavigationFeedbackType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/PassiveNavigationFeedbackType.swift new file mode 100644 index 000000000..80bf038fd --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/PassiveNavigationFeedbackType.swift @@ -0,0 +1,36 @@ +import Foundation + +/// Feedback type is used to specify the type of feedback being recorded with +/// ``NavigationEventsManager/sendPassiveNavigationFeedback(_:type:description:)``. +public enum PassiveNavigationFeedbackType: FeedbackType { + case poorGPS + case incorrectMapData + case accident + case camera + case traffic + case wrongSpeedLimit + case other + + public var typeKey: String { + switch self { + case .other: + return "other_issue" + case .poorGPS: + return "fd_poor_gps" + case .incorrectMapData: + return "fd_incorrect_map_data" + case .accident: + return "fd_accident" + case .camera: + return "fd_camera" + case .traffic: + return "fd_incorrect_traffic" + case .wrongSpeedLimit: + return "fd_wrong_speed_limit" + } + } + + public var subtypeKey: String? { + return nil + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/SearchFeedbackType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/SearchFeedbackType.swift new file mode 100644 index 000000000..ae2b0faec --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/SearchFeedbackType.swift @@ -0,0 +1,35 @@ +import Foundation + +@_documentation(visibility: internal) +public enum SearchFeedbackType: FeedbackType { + case incorrectName + case incorrectAddress + case incorrectLocation + case phoneNumber + case resultRank + case missingResult + case other + + public var typeKey: String { + switch self { + case .missingResult: + return "cannot_find" + case .incorrectName: + return "incorrect_name" + case .incorrectAddress: + return "incorrect_address" + case .incorrectLocation: + return "incorrect_location" + case .other: + return "other_result_issue" + case .phoneNumber: + return "incorrect_phone_number" + case .resultRank: + return "incorrect_result_rank" + } + } + + public var subtypeKey: String? { + return nil + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/AttachmentsUploader.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/AttachmentsUploader.swift new file mode 100644 index 000000000..1d2b5320e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/AttachmentsUploader.swift @@ -0,0 +1,152 @@ +import Foundation +import MapboxCommon + +struct AttachmentArchive { + struct FileType { + var format: String + var type: String + + static var gzip: Self { .init(format: "gz", type: "zip") } + } + + var fileUrl: URL + var fileName: String + var fileId: String + var sessionId: String + var fileType: FileType + var createdAt: Date +} + +protocol AttachmentsUploader { + func upload(accessToken: String, archive: AttachmentArchive) async throws +} + +final class AttachmentsUploaderImpl: AttachmentsUploader { + private enum Constants { +#if DEBUG + static let baseUploadURL = "https://api-events-staging.tilestream.net" +#else + static let baseUploadURL = "https://events.mapbox.com" +#endif + static let mediaTypeZip = "application/zip" + } + + private let dateFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [ + .withInternetDateTime, + .withFractionalSeconds, + .withFullTime, + .withDashSeparatorInDate, + .withColonSeparatorInTime, + .withColonSeparatorInTimeZone, + ] + return formatter + }() + + let options: MapboxCopilot.Options + var sdkInformation: SdkInformation { + options.sdkInformation + } + + private var _filesDir: URL? + private let lock = NSLock() + private var filesDir: URL { + lock.withLock { + if let url = _filesDir { + return url + } + + let cacheDir = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first + ?? NSTemporaryDirectory() + let url = URL(fileURLWithPath: cacheDir, isDirectory: true) + .appendingPathComponent("NavigationHistoryAttachments") + if FileManager.default.fileExists(atPath: url.path) == false { + try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + } + _filesDir = url + return url + } + } + + init(options: MapboxCopilot.Options) { + self.options = options + } + + func upload(accessToken: String, archive: AttachmentArchive) async throws { + let metadata: [String: String] = [ + "name": archive.fileName, + "fileId": archive.fileId, + "sessionId": archive.sessionId, + "format": archive.fileType.format, + "created": dateFormatter.string(from: archive.createdAt), + "type": archive.fileType.type, + ] + let filePath = try prepareFileUpload(of: archive) + let jsonData = (try? JSONEncoder().encode([metadata])) ?? Data() + let jsonString = String(data: jsonData, encoding: .utf8) ?? "" + let log = options.log + return try await withCheckedThrowingContinuation { continuation in + do { + try HttpServiceFactory.getInstance().upload(for: .init( + filePath: filePath, + url: uploadURL(accessToken), + headers: [:], + metadata: jsonString, + mediaType: Constants.mediaTypeZip, + sdkInformation: sdkInformation + )) { [weak self] status in + switch status.state { + case .failed: + let errorMessage = status.error?.message ?? "Unknown upload error" + let error = CopilotError( + errorType: .failedToUploadHistoryFile, + userInfo: ["errorMessage": errorMessage] + ) + log?("Failed to upload session to attachements. \(errorMessage)") + try? self?.cleanupFileUpload(of: archive, from: filePath) + continuation.resume(throwing: error) + case .finished: + try? self?.cleanupFileUpload(of: archive, from: filePath) + continuation.resume() + case .pending, .inProgress: + break + @unknown default: + break + } + } + } catch { + continuation.resume(throwing: error) + } + } + } + + private func prepareFileUpload(of archive: AttachmentArchive) throws -> String { + guard archive.fileUrl.lastPathComponent != archive.fileName else { + return archive.fileUrl.path + } + let fileManager = FileManager.default + let tmpPath = filesDir.appendingPathComponent(archive.fileName).path + + if fileManager.fileExists(atPath: tmpPath) { + try fileManager.removeItem(atPath: tmpPath) + } + + try fileManager.copyItem( + atPath: archive.fileUrl.path, + toPath: tmpPath + ) + return tmpPath + } + + private func cleanupFileUpload(of archive: AttachmentArchive, from temporaryPath: String) throws { + guard archive.fileUrl.path != temporaryPath else { + return + } + try FileManager.default.removeItem(atPath: temporaryPath) + } + + private func uploadURL(_ accessToken: String) throws -> String { + return Constants.baseUploadURL + "/attachments/v1?access_token=\(accessToken)" + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/CopilotService.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/CopilotService.swift new file mode 100644 index 000000000..b8ce5ef06 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/CopilotService.swift @@ -0,0 +1,99 @@ +import Foundation +import MapboxNavigationNative +import UIKit + +public actor CopilotService { + private final class HistoryProviderAdapter: NavigationHistoryProviderProtocol, @unchecked Sendable { + private let historyRecording: HistoryRecording + + init(_ historyRecording: HistoryRecording) { + self.historyRecording = historyRecording + } + + func startRecording() { + historyRecording.startRecordingHistory() + } + + func pushEvent(event: some NavigationHistoryEvent) throws { + try historyRecording.pushHistoryEvent(type: event.eventType, value: event.payload) + } + + func dumpHistory(_ completion: @escaping @Sendable (DumpResult) -> Void) { + historyRecording.stopRecordingHistory(writingFileWith: { url in + guard let url else { + completion(.failure(.noHistory)) + return + } + completion(.success((url.absoluteString, .protobuf))) + }) + } + } + + public private(set) var mapboxCopilot: MapboxCopilot? + + public func setActive(_ isActive: Bool) { + self.isActive = isActive + } + + public private(set) var isActive: Bool { + get { + mapboxCopilot != nil + } + set { + switch (newValue, mapboxCopilot) { + case (true, .none): + activateCopilot() + case (false, .some): + mapboxCopilot = nil + default: + break + } + } + } + + private let accessToken: String + private let navNativeVersion: String + private let historyRecording: HistoryRecording + private let log: (@Sendable (String) -> Void)? + public func setDelegate(_ delegate: MapboxCopilotDelegate) { + self.delegate = delegate + } + + public private(set) weak var delegate: MapboxCopilotDelegate? + + private func activateCopilot() { + Task { + mapboxCopilot = await MapboxCopilot( + options: MapboxCopilot.Options( + accessToken: accessToken, + userId: UIDevice.current.identifierForVendor?.uuidString ?? "-", + navNativeVersion: navNativeVersion, + sdkVersion: Bundle.mapboxNavigationVersion, + sdkName: Bundle.resolvedNavigationSDKName, + packageName: Bundle.mapboxNavigationUXBundleIdentifier, + log: log + ), + historyProvider: HistoryProviderAdapter(historyRecording) + ) + await mapboxCopilot?.setDelegate(delegate) + } + } + + public init( + accessToken: String, + navNativeVersion: String, + historyRecording: HistoryRecording, + isActive: Bool = true, + log: (@Sendable (String) -> Void)? = nil + ) { + self.accessToken = accessToken + self.navNativeVersion = navNativeVersion + self.historyRecording = historyRecording + self.log = log + defer { + Task { + await self.setActive(isActive) + } + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/ApplicationState.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/ApplicationState.swift new file mode 100644 index 000000000..2030ec5dc --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/ApplicationState.swift @@ -0,0 +1,21 @@ +import Foundation + +extension NavigationHistoryEvents { + enum ApplicationState { + case goingToBackground + case goingToForeground + } +} + +extension NavigationHistoryEvents.ApplicationState: NavigationHistoryEvents.Event { + var eventType: String { + switch self { + case .goingToBackground: + return "going_to_background" + case .goingToForeground: + return "going_to_foreground" + } + } + + var payload: String? { nil } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/DriveEnds.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/DriveEnds.swift new file mode 100644 index 000000000..343789d43 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/DriveEnds.swift @@ -0,0 +1,20 @@ +import Foundation + +extension NavigationHistoryEvents { + struct DriveEnds: Event { + enum DriveEndType: String, Encodable { + case applicationClosed = "application_closed" + case vehicleParked = "vehicle_parked" + case arrived + case canceledManually = "canceled_manually" + } + + struct Payload: Encodable { + var type: DriveEndType + var realDuration: Int + } + + let eventType = "drive_ends" + var payload: Payload + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/InitRoute.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/InitRoute.swift new file mode 100644 index 000000000..37bfd72e3 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/InitRoute.swift @@ -0,0 +1,30 @@ +import Foundation + +extension NavigationHistoryEvents { + struct InitRoute: Event { + struct Payload: Encodable { + var requestIdentifier: String? + var route: String + } + + let eventType = "init_route" + let payload: Payload + + init?( + requestIdentifier: String?, + route: Encodable + ) { + let encoder = JSONEncoder() + guard let encodedData = try? encoder.encode(route), + let encodedRoute = String(data: encodedData, encoding: .utf8) + else { + assertionFailure("No route") + return nil + } + self.payload = .init( + requestIdentifier: requestIdentifier, + route: encodedRoute + ) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationFeedback.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationFeedback.swift new file mode 100644 index 000000000..709d17d0a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationFeedback.swift @@ -0,0 +1,15 @@ +import Foundation + +extension NavigationHistoryEvents { + struct NavigationFeedback: Event { + struct Payload: Encodable { + var feedbackId: String + var type: String + var subtype: [String] + var coordinate: Coordinate + } + + let eventType = "nav_feedback_submitted" + var payload: Payload + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationHistoryEvent.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationHistoryEvent.swift new file mode 100644 index 000000000..c79ef8052 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationHistoryEvent.swift @@ -0,0 +1,12 @@ +import Foundation + +public protocol NavigationHistoryEvent { + associatedtype Payload: Encodable + + var eventType: String { get } + var payload: Payload { get } +} + +public enum NavigationHistoryEvents { + typealias Event = NavigationHistoryEvent +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResultUsed.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResultUsed.swift new file mode 100644 index 000000000..98b8a4691 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResultUsed.swift @@ -0,0 +1,62 @@ +import CoreLocation +import Foundation + +extension NavigationHistoryEvents { + public struct Coordinate: Encodable, Sendable { + var latitude: Double + var longitude: Double + + public init(_ coordinate: CLLocationCoordinate2D) { + self.latitude = coordinate.latitude + self.longitude = coordinate.longitude + } + } + + public struct RoutablePoint: Encodable, Sendable { + public var coordinates: Coordinate + + public init(coordinates: Coordinate) { + self.coordinates = coordinates + } + } + + public struct SearchResultUsed: Event, Sendable { + public enum Provider: String, Encodable, Sendable { + case mapbox + } + + public struct Payload: Encodable, Sendable { + public var provider: Provider + public var id: String + + public var name: String + public var address: String + public var coordinate: Coordinate + + public var routablePoint: [RoutablePoint]? + + public init( + provider: Provider, + id: String, + name: String, + address: String, + coordinate: Coordinate, + routablePoint: [RoutablePoint]? + ) { + self.provider = provider + self.id = id + self.name = name + self.address = address + self.coordinate = coordinate + self.routablePoint = routablePoint + } + } + + public let eventType = "search_result_used" + public var payload: Payload + + public init(payload: Payload) { + self.payload = payload + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResults.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResults.swift new file mode 100644 index 000000000..59a4b31b4 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResults.swift @@ -0,0 +1,60 @@ +import CoreLocation +import Foundation + +extension NavigationHistoryEvents { + public struct SearchResults: Event, Sendable { + public struct SearchResult: Encodable, Sendable { + public var id: String + public var name: String + public var address: String + public var coordinate: Coordinate? + public var routablePoint: [RoutablePoint]? + + public init( + id: String, + name: String, + address: String, + coordinate: NavigationHistoryEvents.Coordinate? = nil, + routablePoint: [NavigationHistoryEvents.RoutablePoint]? = nil + ) { + self.id = id + self.name = name + self.address = address + self.coordinate = coordinate + self.routablePoint = routablePoint + } + } + + public struct Payload: Encodable, Sendable { + public var provider: SearchResultUsed.Provider + public var request: String + public var response: String? + public var error: String? + public var searchQuery: String + public var results: [SearchResult]? + + public init( + provider: NavigationHistoryEvents.SearchResultUsed.Provider, + request: String, + response: String? = nil, + error: String? = nil, + searchQuery: String, + results: [NavigationHistoryEvents.SearchResults.SearchResult]? = nil + ) { + self.provider = provider + self.request = request + self.response = response + self.error = error + self.searchQuery = searchQuery + self.results = results + } + } + + public let eventType = "search_results" + public var payload: Payload + + public init(payload: Payload) { + self.payload = payload + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/FeedbackEventsObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/FeedbackEventsObserver.swift new file mode 100644 index 000000000..6ed86733c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/FeedbackEventsObserver.swift @@ -0,0 +1,99 @@ +import Combine +import Foundation +import MapboxCommon_Private + +protocol FeedbackEventsObserver { + var navigationFeedbackPublisher: AnyPublisher { get } + + func refreshSubscription() +} + +final class FeedbackEventsObserverImpl: NSObject, FeedbackEventsObserver { + private let navigationFeedbackSubject = PassthroughSubject() + var navigationFeedbackPublisher: AnyPublisher { + navigationFeedbackSubject.eraseToAnyPublisher() + } + + private var eventsAPI: EventsService? + private let options: MapboxCopilot.Options + private var log: MapboxCopilot.Log? { + options.log + } + + init(options: MapboxCopilot.Options) { + self.options = options + + super.init() + refreshSubscription() + } + + func refreshSubscription() { + let sdkInformation = options.feedbackEventsSdkInformation + let options = EventsServerOptions( + sdkInformation: sdkInformation, + deferredDeliveryServiceOptions: nil + ) + eventsAPI = EventsService.getOrCreate(for: options) + eventsAPI?.registerObserver(for: self) + } + + private func parseEvent(_ attributes: [String: Any]) { + switch attributes["event"] as? String { + case "navigation.feedback": + parseNavigationFeedbackEvent(attributes) + default: + log?("Skipping unknown event with attributes: \(attributes)") + } + } + + private func parseNavigationFeedbackEvent(_ attributes: [String: Any]) { + guard let rawFeedbackId = attributes["feedbackId"], + let rawFeedbackType = attributes["feedbackType"], + let rawLatitude = attributes["lat"], + let rawLongitude = attributes["lng"] + else { + assertionFailure("Failed to parse navigation feedback event") + log?("Failed to fetch required fields for navigation feedback event") + return + } + + do { + let typeConverter = TypeConverter() + let feedbackId = try typeConverter.convert(from: rawFeedbackId, to: String.self) + let feedbackType = try typeConverter.convert(from: rawFeedbackType, to: String.self) + let latitude = try typeConverter.convert(from: rawLatitude, to: Double.self) + let longitude = try typeConverter.convert(from: rawLongitude, to: Double.self) + let feedbackSubtype: [String] = try attributes["feedbackSubType"].map { + try typeConverter.convert(from: $0, to: [String].self) + } ?? [] + let event = NavigationHistoryEvents.NavigationFeedback( + payload: .init( + feedbackId: feedbackId, + type: feedbackType, + subtype: feedbackSubtype, + coordinate: .init(.init(latitude: latitude, longitude: longitude)) + ) + ) + navigationFeedbackSubject.send(event) + } catch { + assertionFailure("Failed to parse navigation feedback event") + } + } +} + +extension FeedbackEventsObserverImpl: EventsServiceObserver { + func didEncounterError(forError error: EventsServiceError, events: Any) { + let eventsDescription = (events as? [[String: Any]])?.description ?? "" + log?("Events Service did encounter error: \(error.message). Events: \(eventsDescription)") + } + + func didSendEvents(forEvents events: Any) { + guard let events = events as? [[String: Any]] else { + assertionFailure("Failed to parse navigation feedback event.") + return + } + for event in events { + parseEvent(event) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilot.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilot.swift new file mode 100644 index 000000000..751f30b60 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilot.swift @@ -0,0 +1,207 @@ +import Foundation +import MapboxCommon +import UIKit + +public actor MapboxCopilot { + public typealias Log = @Sendable (String) -> Void + public struct Options: Sendable { + var accessToken: String + var userId: String = UUID().uuidString + var navNativeVersion: String + var sdkVersion: String + var sdkName: String + var packageName: String + var log: (@Sendable (String) -> Void)? + + var sdkInformation: SdkInformation { + .init( + name: sdkName, + version: sdkVersion, + packageName: packageName + ) + } + + var feedbackEventsSdkInformation: SdkInformation { + .init( + name: "MapboxNavigationNative", + version: navNativeVersion, + packageName: nil + ) + } + + public init( + accessToken: String, + userId: String, + navNativeVersion: String, + sdkVersion: String, + sdkName: String, + packageName: String, + log: (@Sendable (String) -> Void)? = nil + ) { + self.accessToken = accessToken + self.userId = userId + self.navNativeVersion = navNativeVersion + self.sdkVersion = sdkVersion + self.sdkName = sdkName + self.log = log + self.packageName = packageName + } + } + + private let eventsController: NavigationHistoryEventsController + private let manager: NavigationHistoryManager + private let historyProvider: NavigationHistoryProviderProtocol + private let options: Options + + public private(set) var currentSession: NavigationSession? + + public func setDelegate(_ delegate: MapboxCopilotDelegate?) { + self.delegate = delegate + } + + public private(set) weak var delegate: MapboxCopilotDelegate? + + @MainActor + public init( + options: Options, + historyProvider: NavigationHistoryProviderProtocol + ) { + self.init( + options: options, + manager: NavigationHistoryManager( + options: options + ), + historyProvider: historyProvider, + eventsController: NavigationHistoryEventsControllerImpl( + historyProvider: historyProvider, + options: options + ) + ) + } + + init( + options: Options, + manager: NavigationHistoryManager, + historyProvider: NavigationHistoryProviderProtocol, + eventsController: NavigationHistoryEventsController + ) { + self.options = options + self.manager = manager + self.historyProvider = historyProvider + self.eventsController = eventsController + + manager.delegate = self + } + + @discardableResult + public func startActiveGuidanceSession( + requestIdentifier: String?, + route: Encodable, + searchResultUsed: NavigationHistoryEvents.SearchResultUsed? = nil + ) throws -> String { + let session = NavigationSession( + sessionType: .activeGuidance, + accessToken: options.accessToken, + userId: options.userId, + routeId: requestIdentifier, + navNativeVersion: options.navNativeVersion, + navigationVersion: options.sdkVersion + ) + try startSession(session) + try eventsController.startActiveGuidanceSession( + requestIdentifier: requestIdentifier, + route: route, + searchResultUsed: searchResultUsed + ) + return session.id + } + + @discardableResult + public func startFreeDriveSession() throws -> String { + let session = NavigationSession( + sessionType: .freeDrive, + accessToken: options.accessToken, + userId: options.userId, + routeId: nil, + navNativeVersion: options.navNativeVersion, + navigationVersion: options.sdkVersion + ) + try startSession(session) + eventsController.startFreeDriveSession() + return session.id + } + + private func startSession(_ session: NavigationSession) throws { + try completeNavigationSession() + currentSession = session + manager.update(session) + historyProvider.startRecording() + } + + public func arrive() { + eventsController.arrive() + } + + public func completeNavigationSession() throws { + guard var currentSession else { return } + self.currentSession = nil + + currentSession.endedAt = Date() + manager.update(currentSession) + try eventsController.completeSession() + let immutableSession = currentSession + + historyProvider.dumpHistory { [weak self] dump in + Task.detached { [self, immutableSession] in + guard let self else { return } + var currentSession = immutableSession + await self.updateSession(¤tSession, with: dump) + await self.delegate?.copilot(self, didFinishRecording: currentSession) + await self.manager.complete(currentSession) + } + } + } + + public func reportSearchResults(_ event: NavigationHistoryEvents.SearchResults) throws { + try eventsController.reportSearchResults(event) + } + + private func updateSession( + _ session: inout NavigationSession, + with result: NavigationHistoryProviderProtocol.DumpResult + ) { + var format: NavigationHistoryFormat? + var errorString: String? + var fileName: String? + switch result { + case .success(let result): + (fileName, format) = result + case .failure(.noHistory): + errorString = "NN provided no history" + delegate?.copilot(self, didEncounterError: .history(.noHistoryFileProvided, session: session)) + case .failure(.notFound(let path)): + errorString = "History file provided by NN is not found at '\(path)'" + delegate?.copilot(self, didEncounterError: .history(.notFound, session: session, userInfo: ["path": path])) + } + session.historyFormat = format + session.historyError = errorString + session.lastHistoryFileName = fileName + } +} + +extension MapboxCopilot: NavigationHistoryManagerDelegate { + nonisolated func historyManager( + _ historyManager: NavigationHistoryManager, + didUploadHistoryForSession session: NavigationSession + ) { + Task { + await delegate?.copilot(self, didUploadHistoryFileForSession: session) + } + } + + nonisolated func historyManager(_ historyManager: NavigationHistoryManager, didEncounterError error: CopilotError) { + Task { + await delegate?.copilot(self, didEncounterError: error) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilotDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilotDelegate.swift new file mode 100644 index 000000000..dcf159849 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilotDelegate.swift @@ -0,0 +1,14 @@ +import Foundation + +public protocol MapboxCopilotDelegate: AnyObject, Sendable { + func copilot(_ copilot: MapboxCopilot, didFinishRecording session: NavigationSession) + func copilot(_ copilot: MapboxCopilot, didUploadHistoryFileForSession session: NavigationSession) + func copilot(_ copilot: MapboxCopilot, didEncounterError error: CopilotError) +} + +/// Default implementations do nothing +extension MapboxCopilotDelegate { + func copilot(_ copilot: MapboxCopilot, didFinishRecording session: NavigationSession) {} + func copilot(_ copilot: MapboxCopilot, didUploadHistoryFileForSession session: NavigationSession) {} + func copilot(_ copilot: MapboxCopilot, didEncounterError error: CopilotError) {} +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryAttachmentProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryAttachmentProvider.swift new file mode 100644 index 000000000..e65368193 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryAttachmentProvider.swift @@ -0,0 +1,132 @@ +import Foundation + +enum NavigationHistoryAttachmentProvider { + enum Error: Swift.Error { + case noFile + case unsupportedFormat + case noTokenOwner + } + + static func attachementArchive(for session: NavigationSession) throws -> AttachmentArchive { + guard let fileUrl = session.lastHistoryFileUrl else { throw Error.noFile } + + return try .init( + fileUrl: fileUrl, + fileName: session.attachmentFileName(), + fileId: UUID().uuidString, + sessionId: session.attachmentSessionId(), + fileType: .gzip, + createdAt: session.startedAt + ) + } +} + +extension NavigationSession { + var formattedStartedAt: String { + startedAt.metadataValue + } + + var formattedEndedAt: String? { + endedAt?.metadataValue + } +} + +extension NavigationSession { + private typealias Error = NavigationHistoryAttachmentProvider.Error + + fileprivate func attachmentFileName() throws -> String { + guard let historyFormat else { throw Error.unsupportedFormat } + + return Self.composeParts(fallback: "_", separator: "__", escape: ["_", "/"], parts: [ + /* log-start-date */ startedAt.metadataValue, + /* log-end-date */ endedAt?.metadataValue, + /* sdk-platform */ "ios", + /* nav-sdk-version */ navigationSdkVersion, + /* nav-native-sdk-version */ navigationNativeSdkVersion, + /* nav-session-id */ nil, + /* app-version */ appVersion, + /* app-user-id */ userId, + /* app-session-id */ appSessionId, + ]) + ".\(historyFormat.fileExtension)" + } + + fileprivate func attachmentSessionId() throws -> String { + guard let owner = tokenOwner else { throw Error.noTokenOwner } + + return Self.composeParts(fallback: "-", separator: "/", parts: [ + /* unique-prefix */ "co-pilot", owner, + // We can use 1.1 for 1.0 as there are only changes to file name and session id + /* specification-version */ "1.1", + /* app-mode */ appMode, + /* dt */ nil, + /* hr */ nil, + /* drive-mode */ sessionType.metadataValue, + /* telemetry-user-id */ nil, + /* drive-id */ id, + ]) + } + + private static func composeParts( + fallback: String, + separator: String, + escape: [Character] = [], + parts: [String?] + ) -> String { + parts + .map { part in + part.map { escapePart(part: $0, charsToEscape: escape) } + } + .map { $0 ?? fallback } + .joined(separator: separator) + } + + /// Escape characters from `charsToEscape` by prefixing `\` to them + private static func escapePart(part: String, charsToEscape: [Character]) -> String { + charsToEscape.reduce(part) { + $0.replacingOccurrences(of: "\($1)", with: "\\\($1)") + } + } +} + +extension Date { + fileprivate static let metadataFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [ + .withInternetDateTime, + .withFractionalSeconds, + .withFullTime, + .withDashSeparatorInDate, + .withColonSeparatorInTime, + .withColonSeparatorInTimeZone, + ] + return formatter + }() + + fileprivate var metadataValue: String { + Self.metadataFormatter.string(from: self) + } +} + +extension NavigationHistoryFormat { + fileprivate var fileExtension: String { + switch self { + case .json: + return "json" + case .protobuf: + return "pbf.gz" + case .unknown(let ext): + return ext + } + } +} + +extension NavigationSession.SessionType { + var metadataValue: String { + switch self { + case .activeGuidance: + return "active-guidance" + case .freeDrive: + return "free-drive" + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryErrorReport.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryErrorReport.swift new file mode 100644 index 000000000..2d566f2c4 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryErrorReport.swift @@ -0,0 +1,35 @@ +import Foundation + +public struct CopilotError: Error { + enum CopilotErrorType: String { + case noHistoryFileProvided + case notFound + case missingLastHistoryFile + case failedAttachmentsUpload + case failedToUploadHistoryFile + case failedToFetchAccessToken + } + + var errorType: CopilotErrorType + var userInfo: [String: String?]? +} + +extension CopilotError { + static func history( + _ errorType: CopilotErrorType, + session: NavigationSession? = nil, + userInfo: [String: String?]? = nil + ) -> Self { + var userInfo = userInfo + if let session { + var extendedUserInfo = userInfo ?? [:] + extendedUserInfo["sessionId"] = session.id + extendedUserInfo["sessionType"] = session.sessionType.rawValue + if let endedAt = session.endedAt { + extendedUserInfo["duration"] = "\(Int(endedAt.timeIntervalSince(session.startedAt)))" + } + userInfo = extendedUserInfo + } + return Self(errorType: errorType, userInfo: userInfo) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryEventsController.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryEventsController.swift new file mode 100644 index 000000000..53a65391a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryEventsController.swift @@ -0,0 +1,125 @@ +import Combine +import Foundation +import UIKit + +protocol NavigationHistoryEventsController { + func startActiveGuidanceSession( + requestIdentifier: String?, + route: Encodable, + searchResultUsed: NavigationHistoryEvents.SearchResultUsed? + ) throws + func startFreeDriveSession() + func arrive() + func completeSession() throws + + func reportSearchResults(_ event: NavigationHistoryEvents.SearchResults) throws + func resetSearchResults() +} + +final class NavigationHistoryEventsControllerImpl: NavigationHistoryEventsController { + private let historyProvider: NavigationHistoryProviderProtocol + private let feedbackEventsObserver: FeedbackEventsObserver + private let timeProvider: () -> TimeInterval + + private var sessionStartTimestamp: TimeInterval? + private var arrived = false + private var lastSearchResultsEvent: NavigationHistoryEvents.SearchResults? + + private var lifetimeSubscriptions = Set() + + @MainActor + init( + historyProvider: NavigationHistoryProviderProtocol, + options: MapboxCopilot.Options, + feedbackEventsObserver: FeedbackEventsObserver? = nil, + timeProvider: @escaping () -> TimeInterval = { ProcessInfo.processInfo.systemUptime } + ) { + self.historyProvider = historyProvider + self.feedbackEventsObserver = feedbackEventsObserver ?? FeedbackEventsObserverImpl( + options: options + ) + self.timeProvider = timeProvider + + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + + self.feedbackEventsObserver.navigationFeedbackPublisher + .receive(on: DispatchQueue.main) + .sink { + try? historyProvider.pushEvent(event: $0) + } + .store(in: &lifetimeSubscriptions) + } + + func startActiveGuidanceSession( + requestIdentifier: String?, + route: Encodable, + searchResultUsed: NavigationHistoryEvents.SearchResultUsed? + ) throws { + resetSession() + if let event = NavigationHistoryEvents.InitRoute( + requestIdentifier: requestIdentifier, + route: route + ) { + try? historyProvider.pushEvent(event: event) + } + try lastSearchResultsEvent.flatMap { try historyProvider.pushEvent(event: $0) } + try searchResultUsed.flatMap { try historyProvider.pushEvent(event: $0) } + } + + func startFreeDriveSession() { + resetSession() + } + + private func resetSession() { + sessionStartTimestamp = timeProvider() + arrived = false + feedbackEventsObserver.refreshSubscription() + } + + func arrive() { + arrived = true + } + + func completeSession() throws { + if let sessionStartTimestamp { + let duration = timeProvider() - sessionStartTimestamp + self.sessionStartTimestamp = nil + try historyProvider.pushEvent(event: NavigationHistoryEvents.DriveEnds(payload: .init( + type: arrived ? .arrived : .canceledManually, + realDuration: Int(duration * 1e3) + ))) + } + } + + func reportSearchResults(_ event: NavigationHistoryEvents.SearchResults) throws { + lastSearchResultsEvent = event + try historyProvider.pushEvent(event: event) + } + + func resetSearchResults() { + lastSearchResultsEvent = nil + } + + // MARK: - Notifications + + @objc + private func applicationDidEnterBackground() { + try? historyProvider.pushEvent(event: NavigationHistoryEvents.ApplicationState.goingToBackground) + } + + @objc + private func applicationWillEnterForeground() { + try? historyProvider.pushEvent(event: NavigationHistoryEvents.ApplicationState.goingToForeground) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryFormat.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryFormat.swift new file mode 100644 index 000000000..b0ce50a09 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryFormat.swift @@ -0,0 +1,47 @@ +import Foundation + +public enum NavigationHistoryFormat: Codable, Equatable, Sendable { + case json + case protobuf + case unknown(String) + + private static let jsonExt = "json" + private static let protobufExt = "pbf.gz" + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) + switch value { + case Self.jsonExt: + self = .json + case Self.protobufExt: + self = .protobuf + default: + self = .unknown(value) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + let value: String = switch self { + case .json: + Self.jsonExt + case .protobuf: + Self.protobufExt + case .unknown(let ext): + ext + } + try container.encode(value) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.json, .json), (.protobuf, .protobuf): + return true + case (.unknown(let lhsExt), .unknown(let rhsExt)): + return lhsExt == rhsExt + default: + return false + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryLocalStorage.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryLocalStorage.swift new file mode 100644 index 000000000..5194c48e4 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryLocalStorage.swift @@ -0,0 +1,78 @@ +import Foundation + +protocol NavigationHistoryLocalStorageProtocol: Sendable { + func savedSessions() -> [NavigationSession] + func saveSession(_ session: NavigationSession) + func deleteSession(_ session: NavigationSession) +} + +/// Stores history files metadata +final class NavigationHistoryLocalStorage: NavigationHistoryLocalStorageProtocol, @unchecked Sendable { + private static let storageUrl = FileManager.applicationSupportURL + .appendingPathComponent("com.mapbox.Copilot") + .appendingPathComponent("NavigationSessions") + + private let log: MapboxCopilot.Log? + + init(log: MapboxCopilot.Log?) { + self.log = log + } + + func savedSessions() -> [NavigationSession] { + guard let enumerator = FileManager.default.enumerator(at: Self.storageUrl, includingPropertiesForKeys: nil) + else { + return [] + } + + let decoder = JSONDecoder() + let fileUrls = enumerator.compactMap { (element: NSEnumerator.Element) -> URL? in element as? URL } + + var sessions = [NavigationSession]() + for fileUrl in fileUrls { + do { + let data = try Data(contentsOf: fileUrl) + let session = try decoder.decode(NavigationSession.self, from: data) + sessions.append(session) + } catch { + log?("Failed to decode navigation session. Error: \(error). Path: \(fileUrl.absoluteString)") + try? FileManager.default.removeItem(at: fileUrl) + } + } + return sessions + } + + func saveSession(_ session: NavigationSession) { + let fileUrl = storageFileUrl(for: session) + do { + let data = try JSONEncoder().encode(session) + let parentDirectory = fileUrl.deletingLastPathComponent() + if FileManager.default.fileExists(atPath: parentDirectory.path) == false { + try FileManager.default.createDirectory( + at: parentDirectory, + withIntermediateDirectories: true, + attributes: nil + ) + } + try data.write(to: fileUrl, options: .atomic) + } catch { + log?("Failed to save navigation session. Error: \(error). Session id: \(session.id)") + } + } + + func deleteSession(_ session: NavigationSession) { + do { + try FileManager.default.removeItem(at: storageFileUrl(for: session)) + try session.deleteLastHistoryFile() + } catch { + log?("Failed to delete navigation session. Error: \(error). Session id: \(session.id)") + } + } + + static func removeExpiredMetadataFiles(deadline: Date) { + FileManager.default.removeFiles(in: storageUrl, createdBefore: deadline) + } + + private func storageFileUrl(for session: NavigationSession) -> URL { + return Self.storageUrl.appendingPathComponent(session.id).appendingPathExtension("json") + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryManager.swift new file mode 100644 index 000000000..9729d7613 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryManager.swift @@ -0,0 +1,115 @@ +import Combine +import Foundation + +protocol NavigationHistoryManagerDelegate: AnyObject { + func historyManager(_ historyManager: NavigationHistoryManager, didEncounterError error: CopilotError) + func historyManager( + _ historyManager: NavigationHistoryManager, + didUploadHistoryForSession session: NavigationSession + ) +} + +final class NavigationHistoryManager: ObservableObject, @unchecked Sendable { + private enum RemovalPolicy { + static let maxTimeIntervalToKeepHistory: TimeInterval = -24 * 60 * 60 // 1 day + } + + private let localStorage: NavigationHistoryLocalStorageProtocol? + private let uploader: NavigationHistoryUploaderProtocol + private let log: MapboxCopilot.Log? + + weak var delegate: NavigationHistoryManagerDelegate? + + convenience init(options: MapboxCopilot.Options) { + self.init( + localStorage: NavigationHistoryLocalStorage(log: options.log), + uploader: NavigationHistoryUploader(options: options), + log: options.log + ) + } + + init( + localStorage: NavigationHistoryLocalStorageProtocol?, + uploader: NavigationHistoryUploaderProtocol, + log: MapboxCopilot.Log? + ) { + self.localStorage = localStorage + self.uploader = uploader + self.log = log + + loadAndUploadPreviousSessions() + } + + func loadAndUploadPreviousSessions() { + guard let localStorage else { return } + Task.detached { [weak self] in + guard let self else { return } + let restoredSessions = localStorage.savedSessions() + .filter { session in + if self.shouldRetryUpload(session) { + return true + } else { + localStorage.deleteSession(session) + return false + } + } + .sorted(by: { $0.startedAt > $1.startedAt }) + + let removalDeadline = Date().addingTimeInterval(RemovalPolicy.maxTimeIntervalToKeepHistory) + NavigationHistoryLocalStorage.removeExpiredMetadataFiles(deadline: removalDeadline) + for session in restoredSessions { + await upload(session) + } + } + } + + func complete(_ session: NavigationSession) async { + var session = session + session.state = .local + localStorage?.saveSession(session) + await upload(session) + } + + func update(_ session: NavigationSession) { + localStorage?.saveSession(session) + } + + private func upload(_ session: NavigationSession) async { + var session = session + session.state = .uploading + + do { + try await uploader.upload(session, log: log) + delegate?.historyManager(self, didUploadHistoryForSession: session) + localStorage?.deleteSession(session) + } catch { + // We will retry to upload the file on next launch + session.state = .local + delegate?.historyManager(self, didEncounterError: .history( + .failedAttachmentsUpload, + session: session, + userInfo: ["error": error.localizedDescription] + )) + localStorage?.saveSession(session) + } + } + + private func shouldRetryUpload(_ session: NavigationSession) -> Bool { + guard let url = session.lastHistoryFileUrl, FileManager.default.fileExists(atPath: url.path) else { + delegate?.historyManager(self, didEncounterError: .history(.missingLastHistoryFile, session: session)) + return false + } + guard session.startedAt.timeIntervalSinceNow < RemovalPolicy.maxTimeIntervalToKeepHistory else { + // File is too old to be uploaded, we will delete it + return false + } + switch session.state { + case .local, .uploading: + return true + case .inProgress: + // Session might be in `.inProgress` state if it wasn't finished properly (i.e. a crash happened) + // In this case we don't want to upload a potentially corrupted file + return false + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryProvider.swift new file mode 100644 index 000000000..78e5ef758 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryProvider.swift @@ -0,0 +1,15 @@ +import Foundation + +public enum NavigationHistoryProviderError: Error, Sendable { + case noHistory + case notFound(_ path: String) +} + +public protocol NavigationHistoryProviderProtocol: AnyObject { + typealias Filepath = String + typealias DumpResult = Result<(Filepath, NavigationHistoryFormat), NavigationHistoryProviderError> + + func startRecording() + func pushEvent(event: T) throws + func dumpHistory(_ completion: @escaping @Sendable (DumpResult) -> Void) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryUploader.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryUploader.swift new file mode 100644 index 000000000..0b97cc25f --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryUploader.swift @@ -0,0 +1,70 @@ +import Foundation +import UIKit + +protocol NavigationHistoryUploaderProtocol { + func upload(_: NavigationSession, log: MapboxCopilot.Log?) async throws +} + +final class NavigationHistoryUploader: NavigationHistoryUploaderProtocol { + private let attachmentsUploader: AttachmentsUploaderImpl + + init(options: MapboxCopilot.Options) { + self.attachmentsUploader = AttachmentsUploaderImpl(options: options) + } + + @MainActor + func upload(_ session: NavigationSession, log: MapboxCopilot.Log?) async throws { + var backgroundTask: UIBackgroundTaskIdentifier? + let completeBackgroundTask = { + guard let guardedBackgroundTask = backgroundTask else { return } + Task { + UIApplication.shared.endBackgroundTask(guardedBackgroundTask) + } + backgroundTask = nil + } + backgroundTask = UIApplication.shared.beginBackgroundTask( + withName: "Uploading session", + expirationHandler: completeBackgroundTask + ) + defer { completeBackgroundTask() } + try await uploadWithinTask(session, log: log) + } + + @MainActor + private func uploadWithinTask(_ session: NavigationSession, log: MapboxCopilot.Log?) async throws { + do { + try await uploadToAttachments(session: session, log: log) + log?( + "History session uploaded. Type: \(session.sessionType.metadataValue)." + + "Session id: \(session.id)" + ) + } catch { + log?( + "Failed to upload session. Error: \(error). Session id: \(session.id). " + + "Start time: \(session.startedAt)" + ) + throw error + } + } + + @MainActor + private func uploadToAttachments(session: NavigationSession, log: MapboxCopilot.Log?) async throws { + let attachment: AttachmentArchive + do { + attachment = try NavigationHistoryAttachmentProvider.attachementArchive(for: session) + } catch { + log?("Incompatible attachments upload. Session id: \(session.id). Error: \(error)") + throw error + } + + do { + try await attachmentsUploader.upload(accessToken: session.accessToken, archive: attachment) + } catch { + log?( + "Failed to upload history to Attachments API. Error: \(error). Session id: \(session.id)" + + "Start time: \(session.startedAt)" + ) + throw error + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationSession.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationSession.swift new file mode 100644 index 000000000..fbfac6a50 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationSession.swift @@ -0,0 +1,75 @@ +import CoreLocation +import Foundation + +public struct NavigationSession: Codable, Equatable, @unchecked Sendable { + public enum SessionType: String, Codable { + case activeGuidance = "active_guidance" + case freeDrive = "free_drive" + } + + public enum State: String, Codable { + case inProgress = "in_progress" + case local + case uploading + } + + public let id: String + public let startedAt: Date + public let userId: String + public internal(set) var sessionType: SessionType + public internal(set) var accessToken: String + public internal(set) var state: State + public internal(set) var routeId: String? + public internal(set) var endedAt: Date? + public internal(set) var historyError: String? + public internal(set) var appMode: String + public internal(set) var appVersion: String + public internal(set) var navigationSdkVersion: String + public internal(set) var navigationNativeSdkVersion: String + public internal(set) var tokenOwner: String? + public internal(set) var appSessionId: String + var lastHistoryFileName: String? + var historyFormat: NavigationHistoryFormat? + + init( + sessionType: SessionType, + accessToken: String, + userId: String, + routeId: String?, + navNativeVersion: String, + navigationVersion: String + ) { + self.id = UUID().uuidString + self.sessionType = sessionType + self.userId = userId + self.routeId = routeId + self.accessToken = accessToken + + self.startedAt = Date() + self.tokenOwner = TokenOwnerProvider.owner(of: accessToken) + self.appMode = AppEnvironment.applicationMode + self.appVersion = AppEnvironment.hostApplicationVersion() + self.navigationSdkVersion = navigationVersion + self.navigationNativeSdkVersion = navNativeVersion + self.state = .inProgress + self.appSessionId = AppEnvironment.applicationSessionId + } +} + +extension NavigationSession { + @_spi(MapboxInternal) public var _lastHistoryFileName: String? { lastHistoryFileName } +} + +extension NavigationSession { + var lastHistoryFileUrl: URL? { + guard let lastHistoryFileName, lastHistoryFileName.isEmpty == false else { + return nil + } + return URL(string: lastHistoryFileName) + } + + func deleteLastHistoryFile() throws { + guard let url = lastHistoryFileUrl else { return } + try FileManager.default.removeItem(at: url) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/AppEnvironment.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/AppEnvironment.swift new file mode 100644 index 000000000..8d8dfc31d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/AppEnvironment.swift @@ -0,0 +1,28 @@ +import Foundation + +enum AppEnvironment { + static var applicationMode: String { +#if DEBUG + return "mbx-debug" +#else + return "mbx-prod" +#endif + } + + static let applicationSessionId = UUID().uuidString // Unique per application launch +} + +// MARK: - Version resolving + +extension AppEnvironment { + private static let infoPlistShortVersionKey = "CFBundleShortVersionString" + + enum SDK { + case navigation + case navigationNative + } + + static func hostApplicationVersion() -> String { + Bundle.main.infoDictionary?[infoPlistShortVersionKey] as? String ?? "unknown" + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/FileManager++.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/FileManager++.swift new file mode 100644 index 000000000..5e196cb47 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/FileManager++.swift @@ -0,0 +1,27 @@ +import Foundation + +extension FileManager { + static let applicationSupportURL = FileManager.default + .urls(for: .applicationSupportDirectory, in: .userDomainMask) + .first! + + func removeFiles(in directory: URL, createdBefore deadline: Date) { + guard let enumerator = FileManager.default.enumerator( + at: directory, + includingPropertiesForKeys: [.addedToDirectoryDateKey] + ) else { return } + + enumerator + .compactMap { (element: NSEnumerator.Element) -> (url: URL, date: Date)? in + guard let url = element as? URL, + let resourceValues = try? url.resourceValues(forKeys: [.addedToDirectoryDateKey]), + let date = resourceValues.addedToDirectoryDate + else { return nil } + return (url: url, date: date) + } + .filter { $0.date < deadline } + .forEach { + try? FileManager.default.removeItem(atPath: $0.url.path) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TokenOwnerProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TokenOwnerProvider.swift new file mode 100644 index 000000000..951df3de8 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TokenOwnerProvider.swift @@ -0,0 +1,33 @@ +import Foundation + +enum TokenOwnerProvider { + private struct JWTPayload: Decodable { + var u: String + } + + static func owner(of token: String) -> String? { + guard let infoBase64String = token.split(separator: ".").dropFirst().first, + let infoData = base64Decode(String(infoBase64String)), + let info = try? JSONDecoder().decode(JWTPayload.self, from: infoData) + else { + assertionFailure("Failed to parse token.") + return nil + } + return info.u + } + + private static func base64Decode(_ value: String) -> Data? { + var base64 = value + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + + let length = base64.lengthOfBytes(using: .utf8) + let requiredLength = Int(4.0 * ceil(Double(length) / 4.0)) + let paddingLength = requiredLength - length + if paddingLength > 0 { + base64 += String(repeating: "=", count: paddingLength) + } + + return Data(base64Encoded: base64, options: .ignoreUnknownCharacters) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TypeConverter.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TypeConverter.swift new file mode 100644 index 000000000..28565c816 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TypeConverter.swift @@ -0,0 +1,27 @@ +import Foundation + +struct TypeConverter { + func convert( + from fromType: some Any, + to toType: ToType.Type, + file: String = #file, + function: String = #function, + line: Int = #line, + column: Int = #column + ) throws -> ToType { + guard let convertedValue = fromType as? ToType else { + throw NSError( + domain: "com.mapbox.copilot.developerError.failedTypeConversion", + code: -1, + userInfo: [ + "explanation": "Failed to convert \(String(describing: fromType)) to \(toType)", + "file": file, + "function": function, + "line": "\(line)", + "column": "\(column)", + ] + ) + } + return convertedValue + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryEvent.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryEvent.swift new file mode 100644 index 000000000..0b7fddf7e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryEvent.swift @@ -0,0 +1,82 @@ +import CoreLocation +import Foundation +import MapboxNavigationNative + +/// Describes history events produced by ``HistoryReader`` +public protocol HistoryEvent: Equatable, Sendable { + /// Point in time when this event occured. + var timestamp: TimeInterval { get } +} + +extension HistoryEvent { + func compare(to other: any HistoryEvent) -> Bool { + guard let other = other as? Self else { + return false + } + return self == other + } +} + +/// History event of when route was set. +public struct RouteAssignmentHistoryEvent: HistoryEvent { + public let timestamp: TimeInterval + /// ``NavigationRoutes`` that was set. + public let navigationRoutes: NavigationRoutes +} + +/// History event of when location was updated. +public struct LocationUpdateHistoryEvent: HistoryEvent { + /// Point in time when this event occured. + /// + /// This illustrates the moment when history event was recorded. This may differ from + /// ``LocationUpdateHistoryEvent/location``'s `timestamp` since it displays when particular location was reached. + public let timestamp: TimeInterval + /// `CLLocation` being set. + public let location: CLLocation +} + +/// History event of unrecognized type. +/// +/// Such events usually mean that this type of events is not yet supported or this one is for service use only. +public class UnknownHistoryEvent: HistoryEvent, @unchecked Sendable { + public static func == (lhs: UnknownHistoryEvent, rhs: UnknownHistoryEvent) -> Bool { + return lhs.timestamp == rhs.timestamp + } + + public let timestamp: TimeInterval + + init(timestamp: TimeInterval) { + self.timestamp = timestamp + } +} + +final class StatusUpdateHistoryEvent: UnknownHistoryEvent, @unchecked Sendable { + let monotonicTimestamp: TimeInterval + let status: NavigationStatus + + init(timestamp: TimeInterval, monotonicTimestamp: TimeInterval, status: NavigationStatus) { + self.monotonicTimestamp = monotonicTimestamp + self.status = status + + super.init(timestamp: timestamp) + } +} + +/// History event being pushed by the user +/// +/// Such events are created by calling ``HistoryRecording/pushHistoryEvent(type:jsonData:)``. +public struct UserPushedHistoryEvent: HistoryEvent { + public let timestamp: TimeInterval + /// The event type specified for this custom event. + public let type: String + /// The data value that contains a valid JSON attached to the event. + /// + /// This value was provided by user with `HistoryRecording.pushHistoryEvent` method's `dictionary` argument. + public let properties: String + + init(timestamp: TimeInterval, type: String, properties: String) { + self.type = type + self.properties = properties + self.timestamp = timestamp + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReader.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReader.swift new file mode 100644 index 000000000..a51b2a3a0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReader.swift @@ -0,0 +1,198 @@ +import Foundation +import MapboxNavigationNative + +/// Digest of history file contents produced by ``HistoryReader``. +public struct History { + /// Array of recorded events in chronological order. + public fileprivate(set) var events: [any HistoryEvent] = [] + + /// Initial ``NavigationRoutes`` that was set to the Navigator. + /// + /// Can be `nil` if current file is a free drive recording or if history recording was started after such event. In + /// latter case this property may contain another ``NavigationRoutes`` which was, for example, set as a result of a + /// reroute event. + public var initialRoute: NavigationRoutes? { + return (events.first { event in + return event is RouteAssignmentHistoryEvent + } as? RouteAssignmentHistoryEvent)?.navigationRoutes + } + + /// Array of location updates. + public var rawLocations: [CLLocation] { + return events.compactMap { + return ($0 as? LocationUpdateHistoryEvent)?.location + } + } + + func rawLocationsShiftedToPresent() -> [CLLocation] { + return rawLocations.enumerated().map { CLLocation( + coordinate: $0.element.coordinate, + altitude: $0.element.altitude, + horizontalAccuracy: $0.element.horizontalAccuracy, + verticalAccuracy: $0.element.verticalAccuracy, + course: $0.element.course, + speed: $0.element.speed, + timestamp: Date() + TimeInterval($0.offset) + ) } + } +} + +/// Provides event-by-event access to history files contents. +/// +/// Supports `pbf.gz` files. History files are created by ``HistoryRecording/stopRecordingHistory(writingFileWith:)`` +/// and saved to ``HistoryRecordingConfig/historyDirectoryURL``. +public struct HistoryReader: AsyncSequence, Sendable { + public typealias Element = HistoryEvent + + /// Configures ``HistoryReader`` parsing options. + public struct ReadOptions: OptionSet, Sendable { + public var rawValue: UInt + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + /// Reader will skip ``UnknownHistoryEvent`` events. + public static let omitUnknownEvents = ReadOptions(rawValue: 1) + } + + public struct AsyncIterator: AsyncIteratorProtocol { + private let historyReader: MapboxNavigationNative.HistoryReader + private let readOptions: ReadOptions? + + init(historyReader: MapboxNavigationNative.HistoryReader, readOptions: ReadOptions? = nil) { + self.historyReader = historyReader + self.readOptions = readOptions + } + + public mutating func next() async -> (any HistoryEvent)? { + guard let record = historyReader.next() else { + return nil + } + let event = await process(record: record) + if readOptions?.contains(.omitUnknownEvents) ?? false, event is UnknownHistoryEvent { + return await next() + } + return event + } + + private func process(record: HistoryRecord) async -> (any HistoryEvent)? { + let timestamp = TimeInterval(Double(record.timestampNanoseconds) / 1e9) + switch record.type { + case .setRoute: + guard let event = record.setRoute, + let navigationRoutes = await process(setRoute: event) else { break } + return RouteAssignmentHistoryEvent( + timestamp: timestamp, + navigationRoutes: navigationRoutes + ) + case .updateLocation: + guard let event = record.updateLocation else { break } + return LocationUpdateHistoryEvent( + timestamp: timestamp, + location: process(updateLocation: event) + ) + case .getStatus: + guard let event = record.getStatus else { break } + return StatusUpdateHistoryEvent( + timestamp: timestamp, + monotonicTimestamp: TimeInterval(Double( + event + .monotonicTimestampNanoseconds + ) / 1e9), + status: event.result + ) + case .pushHistory: + guard let event = record.pushHistory else { break } + return UserPushedHistoryEvent( + timestamp: timestamp, + type: event.type, + properties: event.properties + ) + @unknown default: + break + } + return UnknownHistoryEvent(timestamp: timestamp) + } + + private func process(setRoute: SetRouteHistoryRecord) async -> NavigationRoutes? { + guard let routeRequest = setRoute.routeRequest, + let routeResponse = setRoute.routeResponse, + routeRequest != "{}", routeResponse != "{}", + let responseData = routeResponse.data(using: .utf8) + else { + // Route reset + return nil + } + let routeIndex = Int(setRoute.routeIndex) + let routes = RouteParser.parseDirectionsResponse( + forResponseDataRef: .init(data: responseData), + request: routeRequest, + routeOrigin: setRoute.origin + ) + + guard routes.isValue(), + var nativeRoutes = routes.value as? [RouteInterface], + nativeRoutes.indices.contains(routeIndex) + else { + assertionFailure("Failed to parse set route event") + return nil + } + let routesData = RouteParser.createRoutesData( + forPrimaryRoute: nativeRoutes.remove(at: routeIndex), + alternativeRoutes: nativeRoutes + ) + return try? await NavigationRoutes(routesData: routesData) + } + + private func process(updateLocation: UpdateLocationHistoryRecord) -> CLLocation { + return CLLocation(updateLocation.location) + } + } + + public func makeAsyncIterator() -> AsyncIterator { + return AsyncIterator( + historyReader: MapboxNavigationNative.HistoryReader(path: fileUrl.path), + readOptions: readOptions + ) + } + + private let fileUrl: URL + private let readOptions: ReadOptions? + + /// Creates a new ``HistoryReader`` + /// + /// - parameter fileUrl: History file to read through. + public init?(fileUrl: URL, readOptions: ReadOptions? = nil) { + guard FileManager.default.fileExists(atPath: fileUrl.path) else { + return nil + } + self.fileUrl = fileUrl + self.readOptions = readOptions + } + + /// Creates a new ``HistoryReader`` instance. + /// + /// - parameter data: History data to read through. + public init?(data: Data, readOptions: ReadOptions? = nil) { + let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + do { + try data.write(to: temporaryURL, options: .withoutOverwriting) + } catch { + return nil + } + self.fileUrl = temporaryURL + self.readOptions = readOptions + } + + /// Performs synchronous full file read. + /// + /// This will read current file from beginning to the end. + /// - returns: ``History`` containing extracted events. + public func parse() async throws -> History { + var result = History() + for await event in self { + result.events.append(event) + } + return result + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecorder.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecorder.swift new file mode 100644 index 000000000..391eefdc7 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecorder.swift @@ -0,0 +1,46 @@ +import MapboxNavigationNative + +struct HistoryRecorder: HistoryRecording, @unchecked Sendable { + private let handle: HistoryRecorderHandle + + init(handle: HistoryRecorderHandle) { + self.handle = handle + } + + func startRecordingHistory() { + Task { @MainActor in + handle.startRecording() + } + } + + func pushHistoryEvent(type: String, jsonData: Data?) { + let jsonString: String + if let jsonData { + guard let value = String(data: jsonData, encoding: .utf8) else { + assertionFailure("Failed to decode string") + return + } + jsonString = value + } else { + jsonString = "" + } + Task { @MainActor in + handle.pushHistory( + forEventType: type, + eventJson: jsonString + ) + } + } + + func stopRecordingHistory(writingFileWith completionHandler: @escaping HistoryFileWritingCompletionHandler) { + Task { @MainActor in + handle.stopRecording { path in + if let path { + completionHandler(URL(fileURLWithPath: path)) + } else { + completionHandler(nil) + } + } + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecording.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecording.swift new file mode 100644 index 000000000..90283b001 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecording.swift @@ -0,0 +1,73 @@ +import Foundation + +/// Types that conform to this protocol record low-level details as the user goes through a trip for debugging purposes. +public protocol HistoryRecording: Sendable { + /// A closure to be called when history writing ends. + /// - Parameters: + /// - historyFileURL: A URL to the file that contains history data. This argument is `nil` if no history data has + /// been written because history recording has not yet begun. Use the + /// ``HistoryRecording/startRecordingHistory()`` method to begin recording before attempting to write a history + /// file. + typealias HistoryFileWritingCompletionHandler = @Sendable (_ historyFileURL: URL?) -> Void + + /// Starts recording history for debugging purposes. + /// + /// - Postcondition: Use the ``HistoryRecording/stopRecordingHistory(writingFileWith:)`` method to stop recording + /// history and write the recorded history to a file. + func startRecordingHistory() + + /// Appends a custom event to the current history log. This can be useful to log things that happen during + /// navigation that are specific to your application. + /// - Parameters: + /// - type: The event type in the events log for your custom event. + /// - jsonData: The data value that contains a valid JSON to attach to the event. + func pushHistoryEvent(type: String, jsonData: Data?) + + /// Stops recording history, asynchronously writing any recorded history to a file. + /// + /// Upon completion, the completion handler is called with the URL to a file in the directory specified by + /// ``HistoryRecordingConfig/historyDirectoryURL``. The file contains details about the passive location manager’s + /// activity that may be useful to include when reporting an issue to Mapbox. + /// - Precondition: Use the ``HistoryRecording/startRecordingHistory()`` method to begin recording history. If the + /// ``HistoryRecording/startRecordingHistory()`` method has not been called, this method has no effect. + /// - Postcondition: To write history incrementally without an interruption in history recording, use the + /// ``HistoryRecording/startRecordingHistory()`` method immediately after this method. If you use the + /// ``HistoryRecording/startRecordingHistory()`` method inside the completion handler of this method, history + /// recording will be paused while the file is being prepared. + /// - Parameter completionHandler: A closure to be executed when the history file is ready. + func stopRecordingHistory(writingFileWith completionHandler: @escaping HistoryFileWritingCompletionHandler) +} + +/// Convenience methods for ``HistoryRecording`` protocol. +extension HistoryRecording { + /// Appends a custom event to the current history log. This can be useful to log things that happen during + /// navigation that are specific to your application. + /// - Precondition: Use the ``HistoryRecording/startRecordingHistory()`` method to begin recording history. If the + /// ``HistoryRecording/startRecordingHistory()`` method has not been called, this method has no effect. + /// - Parameters: + /// - type: The event type in the events log for your custom event. + /// - value: The value that implements `Encodable` protocol and can be encoded into a valid JSON to attach to the + /// event. + /// - encoder: The instance of `JSONEncoder` to be used for the value encoding. If this argument is omitted, the + /// default `JSONEncoder` will be used. + public func pushHistoryEvent(type: String, value: (some Encodable)?, encoder: JSONEncoder? = nil) throws { + let data = try value.map { value -> Data in + try (encoder ?? JSONEncoder()).encode(value) + } + pushHistoryEvent(type: type, jsonData: data) + } + + /// Appends a custom event to the current history log. This can be useful to log things that happen during + /// navigation that are specific to your application. + /// - Precondition: Use the ``HistoryRecording/startRecordingHistory()`` method to begin recording history. If the + /// ``HistoryRecording/startRecordingHistory()`` method has not been called, this method has no effect. + /// - Parameters: + /// - type: The event type in the events log for your custom event. + /// - value: The value disctionary that can be encoded into a JSON to attach to the event. + public func pushHistoryEvent(type: String, dictionary value: [String: Any?]?) throws { + let data = try value.map { value -> Data in + try JSONSerialization.data(withJSONObject: value, options: []) + } + pushHistoryEvent(type: type, jsonData: data) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReplayer.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReplayer.swift new file mode 100644 index 000000000..0c3f3c9e0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReplayer.swift @@ -0,0 +1,351 @@ +import _MapboxNavigationHelpers +import Combine +import CoreLocation +import Foundation + +/// Provides controls over the history replaying playback. +/// +/// This class is used together with ``LocationClient/historyReplayingValue(with:)`` to create and control history files +/// playback. Use this instance to observe playback events, seek, pause and controll playback speed. +public final class HistoryReplayController: Sendable { + private struct State: Sendable { + weak var delegate: HistoryReplayDelegate? + var currentEvent: (any HistoryEvent)? + var isPaused: Bool + var replayStart: TimeInterval? + var speedMultiplier: Double + } + + private let _state: UnfairLocked + + /// ``HistoryReplayDelegate`` instance for observing replay events. + public weak var delegate: HistoryReplayDelegate? { + get { _state.read().delegate } + set { _state.mutate { $0.delegate = newValue } } + } + + /// Playback speed. + /// + /// May be useful for reaching the required portion of the history trace. + /// - important: Too high values may result in navigator not being able to correctly process the events and thus + /// leading to the undefined behavior. Also, the value must be greater than 0. + public var speedMultiplier: Double { + get { _state.read().speedMultiplier } + set { + precondition(newValue > 0.0, "HistoryReplayController.speedMultiplier must be greater than 0!") + _state.mutate { $0.speedMultiplier = newValue } + } + } + + /// A stream of location updates contained in the history trace. + public var locations: AnyPublisher { + _locations.read().eraseToAnyPublisher() + } + + private let _locations: UnfairLocked> = .init(.init()) + private var currentEvent: (any HistoryEvent)? { + get { _state.read().currentEvent } + set { _state.mutate { $0.currentEvent = newValue } } + } + + private let eventsIteratorLocked: UnfairLocked + private func getNextEvent() async -> (any HistoryEvent)? { + var iterator = eventsIteratorLocked.read() + let result = try? await iterator.next() + eventsIteratorLocked.update(iterator) + return result + } + + /// Indicates if the playback was paused. + public var isPaused: Bool { + _state.read().isPaused + } + + private var replayStart: TimeInterval? { + get { _state.read().replayStart } + set { _state.mutate { $0.replayStart = newValue } } + } + + /// Creates new ``HistoryReplayController`` instance. + /// - parameter history: Parsed ``History`` instance, containing stream of events for playback. + public convenience init(history: History) { + self.init( + eventsIterator: .init( + datasource: .historyFile(history, 0) + ) + ) + } + + /// Creates new ``HistoryReplayController`` instance. + /// - parameter historyReader: ``HistoryReader`` instance, used to fetch and replay the history events. + public convenience init(historyReader: HistoryReader) { + self.init( + eventsIterator: .init( + datasource: .historyIterator( + historyReader.makeAsyncIterator() + ) + ) + ) + } + + fileprivate init(eventsIterator: EventsIterator) { + self._state = .init( + .init( + delegate: nil, + currentEvent: nil, + isPaused: true, + replayStart: nil, + speedMultiplier: 1 + ) + ) + self.eventsIteratorLocked = .init(eventsIterator) + } + + /// Seeks forward to the specific event. + /// + /// When found, this event will be the next one processed and reported via the ``HistoryReplayController/delegate``. + /// If such event was not found, controller will seek to the end of the trace. + /// It is not possible to seek backwards. + /// - parameter event: ``HistoryEvent`` to seek to. + /// - returns: `True` if seek was successfull, `False` - otherwise. + public func seekTo(event: any HistoryEvent) async -> Bool { + if replayStart == nil { + currentEvent = await getNextEvent() + replayStart = currentEvent?.timestamp + } + guard replayStart ?? .greatestFiniteMagnitude <= event.timestamp, + currentEvent?.timestamp ?? .greatestFiniteMagnitude <= event.timestamp + else { + return false + } + var nextEvent = if let currentEvent { + currentEvent + } else { + await getNextEvent() + } + + while let checkedEvent = nextEvent, + !checkedEvent.compare(to: event) + { + nextEvent = await getNextEvent() + } + currentEvent = nextEvent + guard nextEvent != nil else { + return false + } + return true + } + + /// Seeks forward to the specific time offset, relative to the beginning of the replay. + /// + /// The next event reported via the ``HistoryReplayController/delegate`` will have it's offset relative to the + /// beginning of the replay be not less then `offset` parameter. + /// It is not possible to seek backwards. + /// - parameter offset: Seek to this offset, relative to the beginning of the replay. If `offset` is greater then + /// replay total duration - controller will seek to the end of the trace. + /// - returns: `True` if seek was successfull, `False` - otherwise. + public func seekTo(offset: TimeInterval) async -> Bool { + if let currentEvent, + let currentOffset = eventOffest(currentEvent), + currentOffset > offset + { + return false + } + + var nextEvent = if let currentEvent { + currentEvent + } else { + await getNextEvent() + } + replayStart = replayStart ?? nextEvent?.timestamp + + while let checkedEvent = nextEvent, + eventOffest(checkedEvent) ?? .greatestFiniteMagnitude < offset + { + nextEvent = await getNextEvent() + } + currentEvent = nextEvent + guard nextEvent != nil else { + return false + } + return true + } + + /// Starts of resumes the playback. + public func play() { + guard isPaused else { return } + _state.mutate { + $0.isPaused = false + } + processEvent(currentEvent) + } + + /// Pauses the playback. + public func pause() { + _state.mutate { + $0.isPaused = true + } + } + + /// Manually pushes the location, as if it was in the replay. + /// + /// May be useful to setup the replay by providing initial location to begin with. + /// - parameter location: `CLLocation` to be pushed through the replay. + public func push(location: CLLocation) { + _locations.read().send(location) + } + + /// Replaces history events in the playback queue. + /// - parameter history: Parsed ``History`` instance, containing stream of events for playback. + public func push(events history: History) { + eventsIteratorLocked.update( + .init( + datasource: .historyFile(history, 0) + ) + ) + } + + /// Replaces history events in the playback queue. + /// - parameter historyReader: ``HistoryReader`` instance, used to fetch and replay the history events. + public func push(events historyReader: HistoryReader) { + eventsIteratorLocked.update( + .init( + datasource: .historyIterator( + historyReader.makeAsyncIterator() + ) + ) + ) + } + + /// Clears the playback queue. + public func clearEvents() { + eventsIteratorLocked.update(.init(datasource: nil)) + currentEvent = nil + replayStart = nil + } + + /// Calculates event's time offset, relative to the beginning of the replay. + /// + /// It does not check if passed event is actually in the replay. The replay must be started (at least 1 event should + /// be processed), before this method could calculate offsets. + /// - parameter event: An event to calculate it's relative time offset. + /// - returns: Event's time offset, relative to the beginning of the replay, or `nil` if current replay was not + /// started yet. + public func eventOffest(_ event: any HistoryEvent) -> TimeInterval? { + replayStart.map { event.timestamp - $0 } + } + + func tick() async { + var eventDelay = currentEvent?.timestamp + currentEvent = await getNextEvent() + guard let currentEvent else { + Task { @MainActor in + delegate?.historyReplayControllerDidFinishReplay(self) + } + return + } + if replayStart == nil { + replayStart = currentEvent.timestamp + } + + eventDelay = currentEvent.timestamp - (eventDelay ?? currentEvent.timestamp) + DispatchQueue.main.asyncAfter(deadline: .now() + (eventDelay ?? 0.0) / speedMultiplier) { [weak self] in + guard let self else { return } + processEvent(currentEvent) + } + } + + private func processEvent(_ event: (any HistoryEvent)?) { + guard !isPaused else { + return + } + defer { + Task.detached { [self] in + await self.tick() + } + } + guard let event else { return } + switch event { + case let locationEvent as LocationUpdateHistoryEvent: + _locations.read().send(locationEvent.location) + case let setRouteEvent as RouteAssignmentHistoryEvent: + delegate?.historyReplayController( + self, + wantsToSetRoutes: setRouteEvent.navigationRoutes + ) + default: + break + } + delegate?.historyReplayController(self, didReplayEvent: event) + } +} + +/// Delegate for ``HistoryReplayController``. +/// +/// Has corresponding methods to observe when particular event has ocurred or when the playback is finished. +public protocol HistoryReplayDelegate: AnyObject, Sendable { + /// Called after each ``HistoryEvent`` was handled by the ``HistoryReplayController``. + /// - parameter controller: A ``HistoryReplayController`` which has handled the event. + /// - parameter event: ``HistoryEvent`` that was just replayed. + func historyReplayController(_ controller: HistoryReplayController, didReplayEvent event: any HistoryEvent) + /// Called when ``HistoryReplayController`` has reached a ``RouteAssignmentHistoryEvent`` and reports that new + /// ``NavigationRoutes`` should be set to the navigator. + /// - parameter controller: A ``HistoryReplayController`` which has handled the event. + /// - parameter navigationRoutes: ``NavigationRoutes`` to be set to the navigator. + func historyReplayController( + _ controller: HistoryReplayController, + wantsToSetRoutes navigationRoutes: NavigationRoutes + ) + /// Called when ``HistoryReplayController`` has reached the end of the replay and finished. + /// - parameter controller: The related ``HistoryReplayController``. + func historyReplayControllerDidFinishReplay(_ controller: HistoryReplayController) +} + +extension LocationClient { + /// Creates a simulation ``LocationClient`` which will replay locations and other events from the history file. + /// - parameter controller: ``HistoryReplayController`` instance used to control and observe the playback. + /// - returns: ``LocationClient``, configured for replaying the history trace. + public static func historyReplayingValue(with controller: HistoryReplayController) -> Self { + return Self( + locations: controller.locations, + headings: Empty().eraseToAnyPublisher(), + startUpdatingLocation: { controller.play() }, + stopUpdatingLocation: { controller.pause() }, + startUpdatingHeading: {}, + stopUpdatingHeading: {} + ) + } +} + +private struct EventsIterator: AsyncIteratorProtocol { + typealias Element = any HistoryEvent + enum Datasource { + case historyIterator(HistoryReader.AsyncIterator) + case historyFile(History, Int) + } + + var datasource: Datasource? + + mutating func next() async throws -> (any HistoryEvent)? { + switch datasource { + case .historyIterator(var asyncIterator): + defer { + self.datasource = .historyIterator(asyncIterator) + } + // This line may trigger bindgen `Function HistoryReader::next called from a thread that is not owning the + // object` error log, if the replayer was instantiated on the main thread. + // This is not an error by itself, but it indicates possible thread safety violation using this iterator. + return await asyncIterator.next() + case .historyFile(let history, let index): + defer { + self.datasource = .historyFile(history, index + 1) + } + guard history.events.indices ~= index else { + return nil + } + return history.events[index] + case .none: + return nil + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/IdleTimerManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/IdleTimerManager.swift new file mode 100644 index 000000000..d97584ec1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/IdleTimerManager.swift @@ -0,0 +1,84 @@ +import _MapboxNavigationHelpers +import Foundation +import UIKit + +/// An idle timer which is managed by `IdleTimerManager`. +protocol IdleTimer: Sendable { + func setDisabled(_ disabled: Bool) +} + +/// UIApplication specific idle timer. +struct UIApplicationIdleTimer: IdleTimer { + func setDisabled(_ disabled: Bool) { + // Using @MainActor task is unsafe as order of Tasks is undefined. DispatchQueue.main is easiest solution. + DispatchQueue.main.async { + UIApplication.shared.isIdleTimerDisabled = disabled + } + } +} + +/// Manages `UIApplication.shared.isIdleTimerDisabled`. +public final class IdleTimerManager: Sendable { + private typealias ID = String + + /// While a cancellable isn't cancelled, the idle timer is disabled. A cancellable is cancelled on deallocation. + public final class Cancellable: Sendable { + private let onCancel: @Sendable () -> Void + + fileprivate init(_ onCancel: @escaping @Sendable () -> Void) { + self.onCancel = onCancel + } + + deinit { + onCancel() + } + } + + public static let shared: IdleTimerManager = .init(idleTimer: UIApplicationIdleTimer()) + + private let idleTimer: IdleTimer + + /// Number of currently non cancelled `IdleTimerManager.Cancellable` instances. + private let cancellablesCount: UnfairLocked = .init(0) + + private let idleTokens: UnfairLocked<[ID: Cancellable]> = .init([:]) + + init(idleTimer: IdleTimer) { + self.idleTimer = idleTimer + } + + /// Disables idle timer `UIApplication.shared.isIdleTimerDisabled` while there is at least one non-cancelled + /// `IdleTimerManager.Cancellable` instance. + /// - Returns: An instance of cancellable that you should retain until you want the idle timer to be disabled. + public func disableIdleTimer() -> IdleTimerManager.Cancellable { + let cancellable = Cancellable { + self.changeCancellabelsCount(delta: -1) + } + changeCancellabelsCount(delta: 1) + return cancellable + } + + /// Disables the idle timer with the specified id. + /// - Parameter id: The id of the timer to disable. + public func disableIdleTimer(id: String) { + idleTokens.mutate { + $0[id] = disableIdleTimer() + } + } + + /// Enables the idle timer with the specified id. + /// - Parameter id: The id of the timer to enable. + public func enableIdleTimer(id: String) { + idleTokens.mutate { + $0[id] = nil + } + } + + private func changeCancellabelsCount(delta: Int) { + let isIdleTimerDisabled = cancellablesCount.mutate { + $0 += delta + return $0 > 0 + } + idleTimer.setDisabled(isIdleTimerDisabled) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Localization/LocalizationManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Localization/LocalizationManager.swift new file mode 100644 index 000000000..4d59f9781 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Localization/LocalizationManager.swift @@ -0,0 +1,48 @@ +import _MapboxNavigationHelpers +import Foundation + +// swiftformat:disable enumNamespaces +/// This class handles the localization of the string inside of the SDK. +public struct LocalizationManager { + /// Set this bundle if you want to provide a custom localization for some string in the SDK. If the provided bundle + /// does not contain the localized version, the string from the default bundle inside the SDK will be used. + public static var customLocalizationBundle: Bundle? { + get { _customLocalizationBundle.read() } + set { _customLocalizationBundle.update(newValue) } + } + + private static let _customLocalizationBundle: NSLocked = .init(nil) + private static let nonExistentKeyValue = "_nonexistent_key_value_".uppercased() + + /// Retrieves the localized string for a given key. + /// - Parameters: + /// - key: The key for the string to localize. + /// - tableName: The name of the table containing the localized string identified by `key`. + /// - defaultBundle: The default bundle containing the table's strings file. + /// - value: The value to use if the key is not found (optional). + /// - comment: A note to the translator describing the context where the localized string is presented to the + /// user. + /// - Returns: A localized string. + public static func localizedString( + _ key: String, + tableName: String? = nil, + defaultBundle: Bundle, + value: String, + comment: String = "" + ) -> String { + if let customBundle = customLocalizationBundle { + let customString = NSLocalizedString( + key, + tableName: tableName, + bundle: customBundle, + value: nonExistentKeyValue, + comment: comment + ) + if customString != nonExistentKeyValue { + return customString + } + } + + return NSLocalizedString(key, tableName: tableName, bundle: defaultBundle, value: value, comment: comment) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Localization/String+Localization.swift b/ios/Classes/Navigation/MapboxNavigationCore/Localization/String+Localization.swift new file mode 100644 index 000000000..36fe246b6 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Localization/String+Localization.swift @@ -0,0 +1,18 @@ +import Foundation + +extension String { + func localizedString( + value: String, + tableName: String? = nil, + defaultBundle: Bundle = .mapboxNavigationUXCore, + comment: String = "" + ) -> String { + LocalizationManager.localizedString( + self, + tableName: tableName, + defaultBundle: defaultBundle, + value: value, + comment: comment + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/CameraStateTransition.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/CameraStateTransition.swift new file mode 100644 index 000000000..5ff60b9e8 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/CameraStateTransition.swift @@ -0,0 +1,34 @@ +import MapboxMaps +import UIKit + +/// Protocol, which is used to execute camera-related transitions, based on data provided via `CameraOptions` in +/// ``ViewportDataSource``. +@MainActor +public protocol CameraStateTransition: AnyObject { + // MARK: Updating the Camera + + /// A map view to which corresponding camera is related. + var mapView: MapView? { get } + + /// Initializer of ``CameraStateTransition`` object. + /// + /// - parameter mapView: `MapView` to which corresponding camera is related. + init(_ mapView: MapView) + + /// Performs a camera transition to new camera options. + /// + /// - parameter cameraOptions: An instance of `CameraOptions`, which describes a viewpoint of the `MapView`. + /// - parameter completion: A completion handler, which is called after performing the transition. + func transitionTo(_ cameraOptions: CameraOptions, completion: @escaping (() -> Void)) + + /// Performs a camera update, when already in the ``NavigationCameraState/overview`` state or + /// ``NavigationCameraState/following`` state. + /// + /// - parameter cameraOptions: An instance of `CameraOptions`, which describes a viewpoint of the `MapView`. + /// - parameter state: An instance of ``NavigationCameraState``, which describes the current state of + /// ``NavigationCamera``. + func update(to cameraOptions: CameraOptions, state: NavigationCameraState) + + /// Cancels the current transition. + func cancelPendingTransition() +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/FollowingCameraOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/FollowingCameraOptions.swift new file mode 100644 index 000000000..d73071fe2 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/FollowingCameraOptions.swift @@ -0,0 +1,260 @@ +import CoreLocation + +/// Options, which are used to control what `CameraOptions` parameters will be modified by +/// ``ViewportDataSource`` in ``NavigationCameraState/following`` state. +public struct FollowingCameraOptions: Equatable, Sendable { + // MARK: Restricting the Orientation + + /// Pitch, which will be taken into account when preparing `CameraOptions` during active guidance + /// navigation. + /// + /// Defaults to `45.0` degrees. + /// + /// - Invariant: Acceptable range of values is `0...85`. + public var defaultPitch: Double = 45.0 { + didSet { + if defaultPitch < 0.0 { + defaultPitch = 0 + assertionFailure("Lower bound of the pitch should not be lower than 0.0") + } + + if defaultPitch > 85.0 { + defaultPitch = 85 + assertionFailure("Upper bound of the pitch should not be higher than 85.0") + } + } + } + + /// Zoom levels range, which will be used when producing camera frame in ``NavigationCameraState/following`` + /// state. + /// + /// Upper bound of the range will be also used as initial zoom level when active guidance navigation starts. + /// + /// Lower bound defaults to `10.50`, upper bound defaults to `16.35`. + /// + /// - Invariant: Acceptable range of values is `0...22`. + public var zoomRange: ClosedRange = 10.50...16.35 { + didSet { + let newValue = zoomRange + + if newValue.lowerBound < 0.0 || newValue.upperBound > 22.0 { + zoomRange = max(0, zoomRange.lowerBound)...min(22, zoomRange.upperBound) + } + + if newValue.lowerBound < 0.0 { + assertionFailure("Lower bound of the zoom range should not be lower than 0.0") + } + + if newValue.upperBound > 22.0 { + assertionFailure("Upper bound of the zoom range should not be higher than 22.0") + } + } + } + + // MARK: Camera Frame Modification Flags + + /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.center` property + /// when producing camera frame in ``NavigationCameraState/following`` state. + /// + /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.center` property. + /// + /// Defaults to `true`. + public var centerUpdatesAllowed = true + + /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.zoom` property + /// when producing camera frame in ``NavigationCameraState/following`` state. + /// + /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.zoom` property. + /// + /// Defaults to `true`. + public var zoomUpdatesAllowed: Bool = true + + /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.bearing` property + /// when producing camera frame in ``NavigationCameraState/following`` state. + /// + /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.bearing` property. + /// + /// Defaults to `true`. + public var bearingUpdatesAllowed: Bool = true + + /// + /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.pitch` property + /// when producing camera frame in ``NavigationCameraState/following`` state. + /// + /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.pitch` property. + /// + /// Defaults to `true`. + public var pitchUpdatesAllowed: Bool = true + + /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.padding` property + /// when producing camera frame in ``NavigationCameraState/following`` state. + /// + /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.padding` property. + /// + /// Defaults to `true`. + public var paddingUpdatesAllowed: Bool = true + + // MARK: Emphasizing the Upcoming Maneuver + + /// Options, which allow to modify the framed route geometries based on the intersection density. + /// + /// By default the whole remainder of the step is framed, while ``IntersectionDensity`` options shrink + /// that geometry to increase the zoom level. + public var intersectionDensity: IntersectionDensity = .init() + + /// Options, which allow to modify `CameraOptions.bearing` property based on information about + /// bearing of an upcoming maneuver. + public var bearingSmoothing: BearingSmoothing = .init() + + /// Options, which allow to modify framed route geometries by appending additional coordinates after + /// maneuver to extend the view. + public var geometryFramingAfterManeuver: GeometryFramingAfterManeuver = .init() + + /// Options, which allow to modify the framed route geometries when approaching a maneuver. + public var pitchNearManeuver: PitchNearManeuver = .init() + + /// If `true`, ``ViewportDataSource`` will follow course of the location. + /// + /// If `false`, ``ViewportDataSource`` will not follow course of the location and use `0.0` value instead. + public var followsLocationCourse = true + + /// Initializes ``FollowingCameraOptions`` instance. + public init() { + // No-op + } + + public static func == (lhs: FollowingCameraOptions, rhs: FollowingCameraOptions) -> Bool { + return lhs.defaultPitch == rhs.defaultPitch && + lhs.zoomRange == rhs.zoomRange && + lhs.centerUpdatesAllowed == rhs.centerUpdatesAllowed && + lhs.zoomUpdatesAllowed == rhs.zoomUpdatesAllowed && + lhs.bearingUpdatesAllowed == rhs.bearingUpdatesAllowed && + lhs.pitchUpdatesAllowed == rhs.pitchUpdatesAllowed && + lhs.paddingUpdatesAllowed == rhs.paddingUpdatesAllowed && + lhs.intersectionDensity == rhs.intersectionDensity && + lhs.bearingSmoothing == rhs.bearingSmoothing && + lhs.geometryFramingAfterManeuver == rhs.geometryFramingAfterManeuver && + lhs.pitchNearManeuver == rhs.pitchNearManeuver && + lhs.followsLocationCourse == rhs.followsLocationCourse + } +} + +/// Options, which allow to modify the framed route geometries based on the intersection density. +/// +/// By default the whole remainder of the step is framed, while `IntersectionDensity` options shrink +/// that geometry to increase the zoom level. +public struct IntersectionDensity: Equatable, Sendable { + /// Controls whether additional coordinates after the upcoming maneuver will be framed + /// to provide the view extension. + /// + /// Defaults to `true`. + public var enabled: Bool = true + + /// Multiplier, which will be used to adjust the size of the portion of the remaining step that's + /// going to be selected for framing. + /// + /// Defaults to `7.0`. + public var averageDistanceMultiplier: Double = 7.0 + + /// Minimum distance between intersections to count them as two instances. + /// + /// This has an effect of filtering out intersections based on parking lot entrances, + /// driveways and alleys from the average intersection distance. + /// + /// Defaults to `20.0` meters. + public var minimumDistanceBetweenIntersections: CLLocationDistance = 20.0 + + /// Initializes `IntersectionDensity` instance. + public init() { + // No-op + } + + public static func == (lhs: IntersectionDensity, rhs: IntersectionDensity) -> Bool { + return lhs.enabled == rhs.enabled && + lhs.averageDistanceMultiplier == rhs.averageDistanceMultiplier && + lhs.minimumDistanceBetweenIntersections == rhs.minimumDistanceBetweenIntersections + } +} + +/// Options, which allow to modify `CameraOptions.bearing` property based on information about +/// bearing of an upcoming maneuver. +public struct BearingSmoothing: Equatable, Sendable { + /// Controls whether bearing smoothing will be performed or not. + /// + /// Defaults to `true`. + public var enabled: Bool = true + + /// Controls how much the bearing can deviate from the location's bearing, in degrees. + /// + /// In case if set, the `bearing` property of `CameraOptions` during active guidance navigation + /// won't exactly reflect the bearing returned by the location, but will also be affected by the + /// direction to the upcoming framed geometry, to maximize the viewable area. + /// + /// Defaults to `45.0` degrees. + public var maximumBearingSmoothingAngle: CLLocationDirection = 45.0 + + /// Initializes ``BearingSmoothing`` instance. + public init() { + // No-op + } + + public static func == (lhs: BearingSmoothing, rhs: BearingSmoothing) -> Bool { + return lhs.enabled == rhs.enabled && + lhs.maximumBearingSmoothingAngle == rhs.maximumBearingSmoothingAngle + } +} + +/// Options, which allow to modify framed route geometries by appending additional coordinates after +/// maneuver to extend the view. +public struct GeometryFramingAfterManeuver: Equatable, Sendable { + /// Controls whether additional coordinates after the upcoming maneuver will be framed + /// to provide the view extension. + /// + /// Defaults to `true`. + public var enabled: Bool = true + + /// Controls the distance between maneuvers closely following the current one to include them + /// in the frame. + /// + /// Defaults to `150.0` meters. + public var distanceToCoalesceCompoundManeuvers: CLLocationDistance = 150.0 + + /// Controls the distance on the route after the current maneuver to include it in the frame. + /// + /// Defaults to `100.0` meters. + public var distanceToFrameAfterManeuver: CLLocationDistance = 100.0 + + /// Initializes ``GeometryFramingAfterManeuver`` instance. + public init() { + // No-op + } + + public static func == (lhs: GeometryFramingAfterManeuver, rhs: GeometryFramingAfterManeuver) -> Bool { + return lhs.enabled == rhs.enabled && + lhs.distanceToCoalesceCompoundManeuvers == rhs.distanceToCoalesceCompoundManeuvers && + lhs.distanceToFrameAfterManeuver == rhs.distanceToFrameAfterManeuver + } +} + +/// Options, which allow to modify the framed route geometries when approaching a maneuver. +public struct PitchNearManeuver: Equatable, Sendable { + /// Controls whether `CameraOptions.pitch` will be set to `0.0` near upcoming maneuver. + /// + /// Defaults to `true`. + public var enabled: Bool = true + + /// Threshold distance to the upcoming maneuver. + /// + /// Defaults to `180.0` meters. + public var triggerDistanceToManeuver: CLLocationDistance = 180.0 + + /// Initializes ``PitchNearManeuver`` instance. + public init() { + // No-op + } + + public static func == (lhs: PitchNearManeuver, rhs: PitchNearManeuver) -> Bool { + return lhs.enabled == rhs.enabled && + lhs.triggerDistanceToManeuver == rhs.triggerDistanceToManeuver + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCamera.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCamera.swift new file mode 100644 index 000000000..382cbbc9f --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCamera.swift @@ -0,0 +1,243 @@ +import _MapboxNavigationHelpers +import Combine +import CoreLocation +import Foundation +import MapboxDirections +import MapboxMaps +import UIKit + +/// ``NavigationCamera`` class provides functionality, which allows to manage camera-related states and transitions in a +/// typical navigation scenarios. It's fed with `CameraOptions` via the ``ViewportDataSource`` protocol and executes +/// transitions using ``CameraStateTransition`` protocol. + +@MainActor +public class NavigationCamera { + struct State: Equatable { + var cameraState: NavigationCameraState = .idle + var location: CLLocation? + var heading: CLHeading? + var routeProgress: RouteProgress? + var viewportPadding: UIEdgeInsets = .zero + } + + /// Notifies that the navigation camera state has changed. + public var cameraStates: AnyPublisher { + _cameraStates.eraseToAnyPublisher() + } + + private let _cameraStates: PassthroughSubject = .init() + + /// The padding applied to the viewport. + public var viewportPadding: UIEdgeInsets { + set { + state.viewportPadding = newValue + } + get { + state.viewportPadding + } + } + + private let _states: PassthroughSubject = .init() + + private var state: State = .init() { + didSet { + _states.send(state) + } + } + + private var lifetimeSubscriptions: Set = [] + private var isTransitioningCameraState: Bool = false + private var lastCameraState: NavigationCameraState = .idle + + /// Initializes ``NavigationCamera`` instance. + /// - Parameters: + /// - mapView: An instance of `MapView`, on which camera-related transitions will be executed. + /// - location: A publisher that emits current user location. + /// - routeProgress: A publisher that emits route navigation progress. + /// - heading: A publisher that emits current user heading. Defaults to `nil.` + /// - navigationCameraType: Type of ``NavigationCamera``, which is used for the current instance of + /// ``NavigationMapView``. + /// - viewportDataSource: An object is used to provide location-related data to perform camera-related updates + /// continuously. + /// - cameraStateTransition: An object, which is used to execute camera transitions. By default + /// ``NavigationCamera`` uses ``NavigationCameraStateTransition``. + public required init( + _ mapView: MapView, + location: AnyPublisher, + routeProgress: AnyPublisher, + heading: AnyPublisher? = nil, + navigationCameraType: NavigationCameraType = .mobile, + viewportDataSource: ViewportDataSource? = nil, + cameraStateTransition: CameraStateTransition? = nil + ) { + self.viewportDataSource = viewportDataSource ?? { + switch navigationCameraType { + case .mobile: + return MobileViewportDataSource(mapView) + case .carPlay: + return CarPlayViewportDataSource(mapView) + } + }() + self.cameraStateTransition = cameraStateTransition ?? NavigationCameraStateTransition(mapView) + observe(location: location) + observe(routeProgress: routeProgress) + observe(heading: heading) + observe(viewportDataSource: self.viewportDataSource) + + _states + .debounce(for: 0.2, scheduler: DispatchQueue.main) + .sink { [weak self] newState in + guard let self else { return } + if let location = newState.location { + self.viewportDataSource.update( + using: ViewportState( + location: location, + routeProgress: newState.routeProgress, + viewportPadding: viewportPadding, + heading: newState.heading + ) + ) + } + if newState.cameraState != lastCameraState { + update(using: newState.cameraState) + } + }.store(in: &lifetimeSubscriptions) + + // Uncomment to be able to see `NavigationCameraDebugView`. +// setupDebugView(mapView) + } + + /// Updates the current camera state. + /// - Parameter cameraState: A new camera state. + public func update(cameraState: NavigationCameraState) { + guard cameraState != state.cameraState else { return } + state.cameraState = cameraState + _cameraStates.send(cameraState) + } + + /// Call to this method immediately moves ``NavigationCamera`` to ``NavigationCameraState/idle`` state and stops all + /// pending transitions. + public func stop() { + update(cameraState: .idle) + cameraStateTransition.cancelPendingTransition() + } + + private var debugView: NavigationCameraDebugView? + + private func setupDebugView(_ mapView: MapView) { + let debugView = NavigationCameraDebugView(mapView, viewportDataSource: viewportDataSource) + self.debugView = debugView + mapView.addSubview(debugView) + } + + private func observe(location: AnyPublisher) { + location.sink { [weak self] in + self?.state.location = $0 + }.store(in: &lifetimeSubscriptions) + } + + private func observe(routeProgress: AnyPublisher) { + routeProgress.sink { [weak self] in + self?.state.routeProgress = $0 + }.store(in: &lifetimeSubscriptions) + } + + private func observe(heading: AnyPublisher?) { + guard let heading else { return } + heading.sink { [weak self] in + self?.state.heading = $0 + }.store(in: &lifetimeSubscriptions) + } + + private var viewportSubscription: [AnyCancellable] = [] + + private func observe(viewportDataSource: ViewportDataSource) { + viewportSubscription = [] + + viewportDataSource.navigationCameraOptions + .removeDuplicates() + .sink { [weak self] navigationCameraOptions in + guard let self else { return } + update(using: navigationCameraOptions) + }.store(in: &viewportSubscription) + + // To prevent the lengthy animation from the Null Island to the current location use the camera to transition to + // the following state. + // The following camera options zoom should be calculated before at the moment. + viewportDataSource.navigationCameraOptions + .filter { $0.followingCamera.zoom != nil } + .first() + .sink { [weak self] _ in + self?.update(cameraState: .following) + }.store(in: &viewportSubscription) + } + + private func update(using cameraState: NavigationCameraState) { + lastCameraState = cameraState + + switch cameraState { + case .idle: + break + case .following: + switchToViewportDatasourceCamera(isFollowing: true) + case .overview: + switchToViewportDatasourceCamera(isFollowing: false) + } + } + + private func cameraOptionsForCurrentState(from navigationCameraOptions: NavigationCameraOptions) -> CameraOptions? { + switch state.cameraState { + case .following: + return navigationCameraOptions.followingCamera + case .overview: + return navigationCameraOptions.overviewCamera + case .idle: + return nil + } + } + + private func update(using navigationCameraOptions: NavigationCameraOptions) { + guard !isTransitioningCameraState, + let options = cameraOptionsForCurrentState(from: navigationCameraOptions) else { return } + + cameraStateTransition.update(to: options, state: state.cameraState) + } + + // MARK: Changing NavigationCamera State + + /// A type, which is used to provide location related data to continuously perform camera-related updates. + /// By default ``NavigationMapView`` uses ``MobileViewportDataSource`` or ``CarPlayViewportDataSource`` depending on + /// the current ``NavigationCameraType``. + public var viewportDataSource: ViewportDataSource { + didSet { + observe(viewportDataSource: viewportDataSource) + } + } + + /// The current state of ``NavigationCamera``. Defaults to ``NavigationCameraState/idle``. + /// + /// Call ``update(cameraState:)`` to update this value. + public var currentCameraState: NavigationCameraState { + state.cameraState + } + + /// A type, which is used to execute camera transitions. + /// By default ``NavigationMapView`` uses ``NavigationCameraStateTransition``. + public var cameraStateTransition: CameraStateTransition + + private func switchToViewportDatasourceCamera(isFollowing: Bool) { + let cameraOptions: CameraOptions = { + if isFollowing { + return viewportDataSource.currentNavigationCameraOptions.followingCamera + } else { + return viewportDataSource.currentNavigationCameraOptions.overviewCamera + } + }() + isTransitioningCameraState = true + cameraStateTransition.transitionTo(cameraOptions) { [weak self] in + self?.isTransitioningCameraState = false + } + } +} + +extension CameraOptions: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraDebugView.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraDebugView.swift new file mode 100644 index 000000000..cd1de7e61 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraDebugView.swift @@ -0,0 +1,199 @@ +import Combine +import MapboxMaps +import UIKit + +/// `UIView`, which is drawn on top of `MapView` and shows `CameraOptions` when ``NavigationCamera`` is in +/// ``NavigationCameraState/following`` state. +/// +/// Such `UIView` is useful for debugging purposes (especially when debugging camera behavior on CarPlay). +class NavigationCameraDebugView: UIView { + weak var mapView: MapView? + + weak var viewportDataSource: ViewportDataSource? { + didSet { + viewportDataSourceLifetimeSubscriptions.removeAll() + subscribe(to: viewportDataSource) + } + } + + private var viewportDataSourceLifetimeSubscriptions: Set = [] + + var viewportLayer = CALayer() + var viewportTextLayer = CATextLayer() + var anchorLayer = CALayer() + var anchorTextLayer = CATextLayer() + var centerLayer = CALayer() + var centerTextLayer = CATextLayer() + var pitchTextLayer = CATextLayer() + var zoomTextLayer = CATextLayer() + var bearingTextLayer = CATextLayer() + var centerCoordinateTextLayer = CATextLayer() + + required init( + _ mapView: MapView, + viewportDataSource: ViewportDataSource? + ) { + self.mapView = mapView + self.viewportDataSource = viewportDataSource + + super.init(frame: mapView.frame) + + isUserInteractionEnabled = false + backgroundColor = .clear + subscribe(to: viewportDataSource) + + viewportLayer.borderWidth = 3.0 + viewportLayer.borderColor = UIColor.green.cgColor + layer.addSublayer(viewportLayer) + + anchorLayer.backgroundColor = UIColor.red.cgColor + anchorLayer.frame = .init(x: 0.0, y: 0.0, width: 6.0, height: 6.0) + anchorLayer.cornerRadius = 3.0 + layer.addSublayer(anchorLayer) + + self.anchorTextLayer = CATextLayer() + anchorTextLayer.string = "Anchor" + anchorTextLayer.fontSize = UIFont.systemFontSize + anchorTextLayer.backgroundColor = UIColor.clear.cgColor + anchorTextLayer.foregroundColor = UIColor.red.cgColor + anchorTextLayer.frame = .zero + layer.addSublayer(anchorTextLayer) + + centerLayer.backgroundColor = UIColor.blue.cgColor + centerLayer.frame = .init(x: 0.0, y: 0.0, width: 6.0, height: 6.0) + centerLayer.cornerRadius = 3.0 + layer.addSublayer(centerLayer) + + self.centerTextLayer = CATextLayer() + centerTextLayer.string = "Center" + centerTextLayer.fontSize = UIFont.systemFontSize + centerTextLayer.backgroundColor = UIColor.clear.cgColor + centerTextLayer.foregroundColor = UIColor.blue.cgColor + centerTextLayer.frame = .zero + layer.addSublayer(centerTextLayer) + + self.pitchTextLayer = createDefaultTextLayer() + layer.addSublayer(pitchTextLayer) + + self.zoomTextLayer = createDefaultTextLayer() + layer.addSublayer(zoomTextLayer) + + self.bearingTextLayer = createDefaultTextLayer() + layer.addSublayer(bearingTextLayer) + + self.viewportTextLayer = createDefaultTextLayer() + layer.addSublayer(viewportTextLayer) + + self.centerCoordinateTextLayer = createDefaultTextLayer() + layer.addSublayer(centerCoordinateTextLayer) + } + + func createDefaultTextLayer() -> CATextLayer { + let textLayer = CATextLayer() + textLayer.string = "" + textLayer.fontSize = UIFont.systemFontSize + textLayer.backgroundColor = UIColor.clear.cgColor + textLayer.foregroundColor = UIColor.black.cgColor + textLayer.frame = .zero + + return textLayer + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func subscribe(to viewportDataSource: ViewportDataSource?) { + viewportDataSource?.navigationCameraOptions + .removeDuplicates() + .sink { [weak self] navigationCameraOptions in + guard let self else { return } + update(using: navigationCameraOptions) + }.store(in: &viewportDataSourceLifetimeSubscriptions) + } + + private func update(using navigationCameraOptions: NavigationCameraOptions) { + guard let mapView else { return } + + let camera = navigationCameraOptions.followingCamera + + if let anchorPosition = camera.anchor { + anchorLayer.position = anchorPosition + anchorTextLayer.frame = .init( + x: anchorLayer.frame.origin.x + 5.0, + y: anchorLayer.frame.origin.y + 5.0, + width: 80.0, + height: 20.0 + ) + } + + if let pitch = camera.pitch { + pitchTextLayer.frame = .init( + x: viewportLayer.frame.origin.x + 5.0, + y: viewportLayer.frame.origin.y + 5.0, + width: viewportLayer.frame.size.width - 10.0, + height: 20.0 + ) + pitchTextLayer.string = "Pitch: \(pitch)º" + } + + if let zoom = camera.zoom { + zoomTextLayer.frame = .init( + x: viewportLayer.frame.origin.x + 5.0, + y: viewportLayer.frame.origin.y + 30.0, + width: viewportLayer.frame.size.width - 10.0, + height: 20.0 + ) + zoomTextLayer.string = "Zoom: \(zoom)" + } + + if let bearing = camera.bearing { + bearingTextLayer.frame = .init( + x: viewportLayer.frame.origin.x + 5.0, + y: viewportLayer.frame.origin.y + 55.0, + width: viewportLayer.frame.size.width - 10.0, + height: 20.0 + ) + bearingTextLayer.string = "Bearing: \(bearing)º" + } + + if let edgeInsets = camera.padding { + viewportLayer.frame = CGRect( + x: edgeInsets.left, + y: edgeInsets.top, + width: mapView.frame.width - edgeInsets.left - edgeInsets.right, + height: mapView.frame.height - edgeInsets.top - edgeInsets.bottom + ) + + viewportTextLayer.frame = .init( + x: viewportLayer.frame.origin.x + 5.0, + y: viewportLayer.frame.origin.y + 80.0, + width: viewportLayer.frame.size.width - 10.0, + height: 20.0 + ) + viewportTextLayer + .string = + "Padding: (top: \(edgeInsets.top), left: \(edgeInsets.left), bottom: \(edgeInsets.bottom), right: \(edgeInsets.right))" + } + + if let centerCoordinate = camera.center { + centerLayer.position = mapView.mapboxMap.point(for: centerCoordinate) + centerTextLayer.frame = .init( + x: centerLayer.frame.origin.x + 5.0, + y: centerLayer.frame.origin.y + 5.0, + width: 80.0, + height: 20.0 + ) + + centerCoordinateTextLayer.frame = .init( + x: viewportLayer.frame.origin.x + 5.0, + y: viewportLayer.frame.origin.y + 105.0, + width: viewportLayer.frame.size.width - 10.0, + height: 20.0 + ) + centerCoordinateTextLayer + .string = "Center coordinate: (lat: \(centerCoordinate.latitude), lng:\(centerCoordinate.longitude))" + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraOptions.swift new file mode 100644 index 000000000..9d5172abb --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraOptions.swift @@ -0,0 +1,22 @@ +import Foundation +import MapboxMaps + +/// Represents calculated navigation camera options. +public struct NavigationCameraOptions: Equatable, Sendable { + /// `CameraOptions`, which are used when transitioning to ``NavigationCameraState/following`` or for continuous + /// updates when already in ``NavigationCameraState/following`` state. + public var followingCamera: CameraOptions + + /// `CameraOptions`, which are used when transitioning to ``NavigationCameraState/overview`` or for continuous + /// updates when already in ``NavigationCameraState/overview`` state. + public var overviewCamera: CameraOptions + + /// Creates a new ``NavigationCameraOptions`` instance. + /// - Parameters: + /// - followingCamera: `CameraOptions` used in the ``NavigationCameraState/following`` state. + /// - overviewCamera: `CameraOptions` used in the``NavigationCameraState/overview`` state. + public init(followingCamera: CameraOptions = .init(), overviewCamera: CameraOptions = .init()) { + self.followingCamera = followingCamera + self.overviewCamera = overviewCamera + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraState.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraState.swift new file mode 100644 index 000000000..4e1707782 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraState.swift @@ -0,0 +1,24 @@ +import MapboxMaps + +/// Defines camera behavior mode. +public enum NavigationCameraState: Equatable, Sendable { + /// The camera position and other attributes are idle. + case idle + /// The camera is following user position. + case following + /// The camera is previewing some extended, non-point object. + case overview +} + +extension NavigationCameraState: CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .idle: + return "idle" + case .following: + return "following" + case .overview: + return "overview" + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraStateTransition.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraStateTransition.swift new file mode 100644 index 000000000..181cea65d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraStateTransition.swift @@ -0,0 +1,209 @@ +import MapboxMaps +import Turf +import UIKit + +/// The class, which conforms to ``CameraStateTransition`` protocol and provides default implementation of +/// camera-related transitions by using `CameraAnimator` functionality provided by Mapbox Maps SDK. +@MainActor +public class NavigationCameraStateTransition: CameraStateTransition { + // MARK: Transitioning State + + /// A map view to which corresponding camera is related. + public weak var mapView: MapView? + + var animatorCenter: BasicCameraAnimator? + var animatorZoom: BasicCameraAnimator? + var animatorBearing: BasicCameraAnimator? + var animatorPitch: BasicCameraAnimator? + var animatorAnchor: BasicCameraAnimator? + var animatorPadding: BasicCameraAnimator? + + var previousAnchor: CGPoint = .zero + + /// Initializer of ``NavigationCameraStateTransition`` object. + /// - Parameter mapView: `MapView` to which corresponding camera is related. + public required init(_ mapView: MapView) { + self.mapView = mapView + } + + /// Performs a camera transition to new camera options. + /// + /// - parameter cameraOptions: An instance of `CameraOptions`, which describes a viewpoint of the `MapView`. + /// - parameter completion: A completion handler, which is called after performing the transition. + public func transitionTo( + _ cameraOptions: CameraOptions, + completion: @escaping () -> Void + ) { + guard let mapView, + let zoom = cameraOptions.zoom + else { + completion() + return + } + + if let center = cameraOptions.center, !CLLocationCoordinate2DIsValid(center) { + completion() + return + } + + stopAnimators() + let duration: TimeInterval = mapView.mapboxMap.cameraState.zoom < zoom ? 0.5 : 0.25 + mapView.camera.fly(to: cameraOptions, duration: duration) { _ in + completion() + } + } + + /// Cancels the current transition. + public func cancelPendingTransition() { + stopAnimators() + } + + /// Performs a camera update, when already in the ``NavigationCameraState/overview`` state + /// or ``NavigationCameraState/following`` state. + /// + /// - parameter cameraOptions: An instance of `CameraOptions`, which describes a viewpoint of the `MapView`. + /// - parameter state: An instance of ``NavigationCameraState``, which describes the current state of + /// ``NavigationCamera``. + public func update(to cameraOptions: CameraOptions, state: NavigationCameraState) { + guard let mapView, + let center = cameraOptions.center, + CLLocationCoordinate2DIsValid(center), + let zoom = cameraOptions.zoom, + let bearing = (state == .overview) ? 0.0 : cameraOptions.bearing, + let pitch = cameraOptions.pitch, + let anchor = cameraOptions.anchor, + let padding = cameraOptions.padding else { return } + + let duration = 1.0 + let minimumCenterCoordinatePixelThreshold = 2.0 + let minimumPitchThreshold: CGFloat = 1.0 + let minimumBearingThreshold: CLLocationDirection = 1.0 + let timingParameters = UICubicTimingParameters( + controlPoint1: CGPoint(x: 0.0, y: 0.0), + controlPoint2: CGPoint(x: 1.0, y: 1.0) + ) + + // Check whether the location change is larger than a certain threshold when current camera state is following. + var updateCameraCenter = true + if state == .following { + let metersPerPixel = getMetersPerPixelAtLatitude(center.latitude, Double(zoom)) + let centerUpdateThreshold = minimumCenterCoordinatePixelThreshold * metersPerPixel + updateCameraCenter = (mapView.mapboxMap.cameraState.center.distance(to: center) > centerUpdateThreshold) + } + + if updateCameraCenter { + if let animatorCenter, animatorCenter.isRunning { + animatorCenter.stopAnimation() + } + + animatorCenter = mapView.camera.makeAnimator( + duration: duration, + timingParameters: timingParameters + ) { transition in + transition.center.toValue = center + } + + animatorCenter?.startAnimation() + } + + if let animatorZoom, animatorZoom.isRunning { + animatorZoom.stopAnimation() + } + + animatorZoom = mapView.camera.makeAnimator( + duration: duration, + timingParameters: timingParameters + ) { transition in + transition.zoom.toValue = zoom + } + + animatorZoom?.startAnimation() + + // Check whether the bearing change is larger than a certain threshold when current camera state is following. + let updateCameraBearing = (state == .following) ? + (abs(mapView.mapboxMap.cameraState.bearing - bearing) >= minimumBearingThreshold) : true + + if updateCameraBearing { + if let animatorBearing, animatorBearing.isRunning { + animatorBearing.stopAnimation() + } + + animatorBearing = mapView.camera.makeAnimator( + duration: duration, + timingParameters: timingParameters + ) { transition in + transition.bearing.toValue = bearing + } + + animatorBearing?.startAnimation() + } + + // Check whether the pitch change is larger than a certain threshold when current camera state is following. + let updateCameraPitch = (state == .following) ? + (abs(mapView.mapboxMap.cameraState.pitch - pitch) >= minimumPitchThreshold) : true + + if updateCameraPitch { + if let animatorPitch, animatorPitch.isRunning { + animatorPitch.stopAnimation() + } + + animatorPitch = mapView.camera.makeAnimator( + duration: duration, + timingParameters: timingParameters + ) { transition in + transition.pitch.toValue = pitch + } + + animatorPitch?.startAnimation() + } + + // In case if anchor did not change - do not perform animation. + let updateCameraAnchor = previousAnchor != anchor + previousAnchor = anchor + + if updateCameraAnchor { + if let animatorAnchor, animatorAnchor.isRunning { + animatorAnchor.stopAnimation() + } + + animatorAnchor = mapView.camera.makeAnimator( + duration: duration, + timingParameters: timingParameters + ) { transition in + transition.anchor.toValue = anchor + } + + animatorAnchor?.startAnimation() + } + + if let animatorPadding, animatorPadding.isRunning { + animatorPadding.stopAnimation() + } + + animatorPadding = mapView.camera.makeAnimator( + duration: duration, + timingParameters: timingParameters + ) { transition in + transition.padding.toValue = padding + } + + animatorPadding?.startAnimation() + } + + func stopAnimators() { + let animators = [ + animatorCenter, + animatorZoom, + animatorBearing, + animatorPitch, + animatorAnchor, + animatorPadding, + ] + mapView?.camera.cancelAnimations() + animators.compactMap { $0 }.forEach { + if $0.isRunning { + $0.stopAnimation() + } + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraType.swift new file mode 100644 index 000000000..72a8a940c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraType.swift @@ -0,0 +1,8 @@ +/// Possible types of ``NavigationCamera``. +public enum NavigationCameraType { + /// When such type is used `MapboxMaps.CameraOptions` will be optimized specifically for CarPlay devices. + case carPlay + + /// Type, which is used for iPhone/iPad. + case mobile +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationViewportDataSourceOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationViewportDataSourceOptions.swift new file mode 100644 index 000000000..cf24c2e47 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationViewportDataSourceOptions.swift @@ -0,0 +1,36 @@ +import Foundation + +/// Options, which give the ability to control whether certain `CameraOptions` will be generated +/// by ``ViewportDataSource`` or can be provided by user directly. +public struct NavigationViewportDataSourceOptions: Equatable, Sendable { + /// Options, which are used to control what `CameraOptions` parameters will be modified by + /// ``ViewportDataSource`` in ``NavigationCameraState/following`` state. + public var followingCameraOptions = FollowingCameraOptions() + + /// Options, which are used to control what `CameraOptions` parameters will be modified by + /// ``ViewportDataSource`` in ``NavigationCameraState/overview`` state. + public var overviewCameraOptions = OverviewCameraOptions() + + /// Initializes `NavigationViewportDataSourceOptions` instance. + public init() { + // No-op + } + + /// Initializes `NavigationViewportDataSourceOptions` instance. + /// + /// - parameter followingCameraOptions: `FollowingCameraOptions` instance, which contains + /// `CameraOptions` parameters, which in turn will be used by ``ViewportDataSource`` in + /// ``NavigationCameraState/following`` state. + /// - parameter overviewCameraOptions: `OverviewCameraOptions` instance, which contains + /// `CameraOptions` parameters, which it turn will be used by ``ViewportDataSource`` in + /// ``NavigationCameraState/overview`` state. + public init(followingCameraOptions: FollowingCameraOptions, overviewCameraOptions: OverviewCameraOptions) { + self.followingCameraOptions = followingCameraOptions + self.overviewCameraOptions = overviewCameraOptions + } + + public static func == (lhs: NavigationViewportDataSourceOptions, rhs: NavigationViewportDataSourceOptions) -> Bool { + return lhs.followingCameraOptions == rhs.followingCameraOptions && + lhs.overviewCameraOptions == rhs.overviewCameraOptions + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/OverviewCameraOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/OverviewCameraOptions.swift new file mode 100644 index 000000000..479b268fb --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/OverviewCameraOptions.swift @@ -0,0 +1,78 @@ +import Foundation + +/// Options, which are used to control what `CameraOptions` parameters will be modified by +/// ``ViewportDataSource`` in ``NavigationCameraState/overview`` state. +public struct OverviewCameraOptions: Equatable, Sendable { + /// Maximum zoom level, which will be used when producing camera frame in ``NavigationCameraState/overview`` + /// state. + /// + /// Defaults to `16.35`. + /// + /// - Invariant: Acceptable range of values is 0...22. + public var maximumZoomLevel: Double = 16.35 { + didSet { + if maximumZoomLevel < 0.0 { + maximumZoomLevel = 0 + assertionFailure("Maximum zoom level should not be lower than 0.0") + } + + if maximumZoomLevel > 22.0 { + maximumZoomLevel = 22 + assertionFailure("Maximum zoom level should not be higher than 22.0") + } + } + } + + /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.center` property + /// when producing camera frame in ``NavigationCameraState/overview`` state. + /// + /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.center` property. + /// + /// Defaults to `true`. + public var centerUpdatesAllowed: Bool = true + + /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.zoom` property + /// when producing camera frame in ``NavigationCameraState/overview`` state. + /// + /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.zoom` property. + /// + /// Defaults to `true`. + public var zoomUpdatesAllowed: Bool = true + + /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.bearing` property + /// when producing camera frame in ``NavigationCameraState/overview`` state. + /// + /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.bearing` property. + /// + /// Defaults to `true`. + public var bearingUpdatesAllowed: Bool = true + + /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.pitch` property + /// when producing camera frame in ``NavigationCameraState/overview`` state. + /// + /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.pitch` property. + /// + /// Defaults to `true`. + public var pitchUpdatesAllowed: Bool = true + + /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.padding` property + /// when producing camera frame in ``NavigationCameraState/overview`` state. + /// + /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.padding` property. + /// + /// Defaults to `true`. + public var paddingUpdatesAllowed: Bool = true + + /// Initializes ``OverviewCameraOptions`` instance. + public init() { + // No-op + } + + public static func == (lhs: OverviewCameraOptions, rhs: OverviewCameraOptions) -> Bool { + return lhs.maximumZoomLevel == rhs.maximumZoomLevel && + lhs.zoomUpdatesAllowed == rhs.zoomUpdatesAllowed && + lhs.bearingUpdatesAllowed == rhs.bearingUpdatesAllowed && + lhs.pitchUpdatesAllowed == rhs.pitchUpdatesAllowed && + lhs.paddingUpdatesAllowed == rhs.paddingUpdatesAllowed + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CarPlayViewportDataSource.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CarPlayViewportDataSource.swift new file mode 100644 index 000000000..defe5abf7 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CarPlayViewportDataSource.swift @@ -0,0 +1,312 @@ +import Combine +import CoreLocation +import MapboxDirections +import MapboxMaps +import Turf +import UIKit + +/// The class, which conforms to ``ViewportDataSource`` protocol and provides default implementation of it for CarPlay. +@MainActor +public class CarPlayViewportDataSource: ViewportDataSource { + private var commonDataSource: CommonViewportDataSource + + weak var mapView: MapView? + + /// Initializes ``CarPlayViewportDataSource`` instance. + /// - Parameter mapView: An instance of `MapView`, which is going to be used for viewport calculation. `MapView` + /// will be weakly stored by ``CarPlayViewportDataSource``. + public required init(_ mapView: MapView) { + self.mapView = mapView + self.commonDataSource = .init(mapView) + } + + /// Options, which give the ability to control whether certain `CameraOptions` will be generated. + public var options: NavigationViewportDataSourceOptions { + get { commonDataSource.options } + set { commonDataSource.options = newValue } + } + + /// Notifies that the navigation camera options have changed in response to a viewport change. + public var navigationCameraOptions: AnyPublisher { + commonDataSource.navigationCameraOptions + } + + /// The last calculated ``NavigationCameraOptions``. + public var currentNavigationCameraOptions: NavigationCameraOptions { + get { + commonDataSource.currentNavigationCameraOptions + } + + set { + commonDataSource.currentNavigationCameraOptions = newValue + } + } + + /// Updates ``NavigationCameraOptions`` accoridng to the navigation state. + /// - Parameters: + /// - viewportState: The current viewport state. + public func update(using viewportState: ViewportState) { + commonDataSource.update(using: viewportState) { [weak self] state in + guard let self else { return nil } + return NavigationCameraOptions( + followingCamera: newFollowingCamera(with: state), + overviewCamera: newOverviewCamera(with: state) + ) + } + } + + private func newFollowingCamera(with state: ViewportDataSourceState) -> CameraOptions { + guard let mapView else { return .init() } + + let followingCameraOptions = options.followingCameraOptions + + var newOptions = currentNavigationCameraOptions.followingCamera + if let location = state.location, state.navigationState.isInPassiveNavigationOrCompletedActive { + if followingCameraOptions.centerUpdatesAllowed || followingCamera.center == nil { + newOptions.center = location.coordinate + } + + if followingCameraOptions.zoomUpdatesAllowed || followingCamera.zoom == nil { + let altitude = 1700.0 + let zoom = CGFloat(ZoomLevelForAltitude( + altitude, + mapView.mapboxMap.cameraState.pitch, + location.coordinate.latitude, + mapView.bounds.size + )) + + newOptions.zoom = zoom + } + + if followingCameraOptions.bearingUpdatesAllowed || followingCamera.bearing == nil { + if followingCameraOptions.followsLocationCourse { + newOptions.bearing = location.course + } else { + newOptions.bearing = 0.0 + } + } + + newOptions.anchor = mapView.center + + if followingCameraOptions.pitchUpdatesAllowed || followingCamera.pitch == nil { + newOptions.pitch = 0.0 + } + + if followingCameraOptions.paddingUpdatesAllowed || followingCamera.padding == nil { + newOptions.padding = mapView.safeAreaInsets + } + + return newOptions + } + + if let location = state.location, case .active(let activeState) = state.navigationState, + !activeState.isRouteComplete + { + let coordinatesToManeuver = activeState.coordinatesToManeuver + let lookaheadDistance = activeState.lookaheadDistance + + var compoundManeuvers: [[CLLocationCoordinate2D]] = [] + let geometryFramingAfterManeuver = followingCameraOptions.geometryFramingAfterManeuver + let pitchСoefficient = pitchСoefficient( + distanceRemainingOnStep: activeState.distanceRemainingOnStep, + currentCoordinate: location.coordinate, + currentLegStepIndex: activeState.currentLegStepIndex, + currentLegSteps: activeState.currentLegSteps + ) + let pitch = followingCameraOptions.defaultPitch * pitchСoefficient + var carPlayCameraPadding = mapView.safeAreaInsets + UIEdgeInsets.centerEdgeInsets + + // Bottom of the viewport on CarPlay should be placed at the same level with + // trip estimate view. + carPlayCameraPadding.bottom += 65.0 + + if geometryFramingAfterManeuver.enabled { + let nextStepIndex = min(activeState.currentLegStepIndex + 1, activeState.currentLegSteps.count - 1) + + var totalDistance: CLLocationDistance = 0.0 + for (index, step) in activeState.currentLegSteps.dropFirst(nextStepIndex).enumerated() { + guard let stepCoordinates = step.shape?.coordinates, + let distance = stepCoordinates.distance() else { continue } + + if index == 0 { + if distance >= geometryFramingAfterManeuver.distanceToFrameAfterManeuver { + let trimmedStepCoordinates = stepCoordinates + .trimmed(distance: geometryFramingAfterManeuver.distanceToFrameAfterManeuver) + compoundManeuvers.append(trimmedStepCoordinates) + break + } else { + compoundManeuvers.append(stepCoordinates) + totalDistance += distance + } + } else if distance >= 0.0, totalDistance < geometryFramingAfterManeuver + .distanceToCoalesceCompoundManeuvers + { + if distance + totalDistance >= geometryFramingAfterManeuver + .distanceToCoalesceCompoundManeuvers + { + let remanentDistance = geometryFramingAfterManeuver + .distanceToCoalesceCompoundManeuvers - totalDistance + let trimmedStepCoordinates = stepCoordinates.trimmed(distance: remanentDistance) + compoundManeuvers.append(trimmedStepCoordinates) + break + } else { + compoundManeuvers.append(stepCoordinates) + totalDistance += distance + } + } + } + } + + let coordinatesForManeuverFraming = compoundManeuvers.reduce([], +) + var coordinatesToFrame = coordinatesToManeuver.sliced( + from: nil, + to: LineString(coordinatesToManeuver).coordinateFromStart(distance: lookaheadDistance) + ) + let pitchNearManeuver = followingCameraOptions.pitchNearManeuver + if pitchNearManeuver.enabled, + activeState.distanceRemainingOnStep <= pitchNearManeuver.triggerDistanceToManeuver + { + coordinatesToFrame += coordinatesForManeuverFraming + } + + if options.followingCameraOptions.centerUpdatesAllowed || followingCamera.center == nil { + var center = location.coordinate + if let boundingBox = BoundingBox(from: coordinatesToFrame) { + let coordinates = [ + center, + [boundingBox.northEast, boundingBox.southWest].centerCoordinate, + ] + + let centerLineString = LineString(coordinates) + let centerLineStringTotalDistance = centerLineString.distance() ?? 0.0 + let centerCoordDistance = centerLineStringTotalDistance * (1 - pitchСoefficient) + if let adjustedCenter = centerLineString.coordinateFromStart(distance: centerCoordDistance) { + center = adjustedCenter + } + } + + newOptions.center = center + } + + if options.followingCameraOptions.zoomUpdatesAllowed || followingCamera.zoom == nil { + let defaultZoomLevel = 12.0 + let followingCarPlayCameraZoom = zoom( + coordinatesToFrame, + mapView: mapView, + pitch: pitch, + maxPitch: followingCameraOptions.defaultPitch, + edgeInsets: carPlayCameraPadding, + defaultZoomLevel: defaultZoomLevel, + maxZoomLevel: followingCameraOptions.zoomRange.upperBound, + minZoomLevel: followingCameraOptions.zoomRange.lowerBound + ) + newOptions.zoom = followingCarPlayCameraZoom + } + + if options.followingCameraOptions.bearingUpdatesAllowed || followingCamera.bearing == nil { + var bearing = location.course + let distance = fmax( + lookaheadDistance, + geometryFramingAfterManeuver.enabled + ? geometryFramingAfterManeuver.distanceToCoalesceCompoundManeuvers + : 0.0 + ) + let coordinatesForIntersections = coordinatesToManeuver.sliced( + from: nil, + to: LineString(coordinatesToManeuver) + .coordinateFromStart(distance: distance) + ) + + bearing = self.bearing( + location.course, + mapView: mapView, + coordinatesToManeuver: coordinatesForIntersections + ) + newOptions.bearing = bearing + } + + let followingCarPlayCameraAnchor = anchor( + pitchСoefficient, + bounds: mapView.bounds, + edgeInsets: carPlayCameraPadding + ) + + newOptions.anchor = followingCarPlayCameraAnchor + + if options.followingCameraOptions.pitchUpdatesAllowed || followingCamera.pitch == nil { + newOptions.pitch = CGFloat(pitch) + } + + if options.followingCameraOptions.paddingUpdatesAllowed || followingCamera.padding == nil { + if mapView.window?.screen.traitCollection.userInterfaceIdiom == .carPlay { + newOptions.padding = UIEdgeInsets( + top: followingCarPlayCameraAnchor.y, + left: carPlayCameraPadding.left, + bottom: mapView.bounds + .height - followingCarPlayCameraAnchor.y + 1.0, + right: carPlayCameraPadding.right + ) + } else { + newOptions.padding = carPlayCameraPadding + } + } + } + return newOptions + } + + private func newOverviewCamera(with state: ViewportDataSourceState) -> CameraOptions { + guard let mapView else { return .init() } + + // In active guidance navigation, camera in overview mode is relevant, during free-drive + // navigation it's not used. + guard case .active(let activeState) = state.navigationState else { return overviewCamera } + + var newOptions = currentNavigationCameraOptions.overviewCamera + let remainingCoordinatesOnRoute = activeState.remainingCoordinatesOnRoute + + let carPlayCameraPadding = mapView.safeAreaInsets + UIEdgeInsets.centerEdgeInsets + let overviewCameraOptions = options.overviewCameraOptions + + if overviewCameraOptions.pitchUpdatesAllowed || overviewCamera.pitch == nil { + newOptions.pitch = 0.0 + } + + if overviewCameraOptions.centerUpdatesAllowed || overviewCamera.center == nil { + if let boundingBox = BoundingBox(from: remainingCoordinatesOnRoute) { + let center = [ + boundingBox.southWest, + boundingBox.northEast, + ].centerCoordinate + + newOptions.center = center + } + } + + newOptions.anchor = anchor( + bounds: mapView.bounds, + edgeInsets: carPlayCameraPadding + ) + + if overviewCameraOptions.bearingUpdatesAllowed || overviewCamera.bearing == nil { + // In case if `NavigationCamera` is already in ``NavigationCameraState/overview`` value + // of bearing will be also ignored. + newOptions.bearing = 0.0 + } + + if overviewCameraOptions.zoomUpdatesAllowed || overviewCamera.zoom == nil { + newOptions.zoom = overviewCameraZoom( + remainingCoordinatesOnRoute, + mapView: mapView, + pitch: newOptions.pitch, + bearing: newOptions.bearing, + edgeInsets: carPlayCameraPadding, + maxZoomLevel: overviewCameraOptions.maximumZoomLevel + ) + } + + if overviewCameraOptions.paddingUpdatesAllowed || overviewCamera.padding == nil { + newOptions.padding = carPlayCameraPadding + } + return newOptions + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CommonViewportDataSource.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CommonViewportDataSource.swift new file mode 100644 index 000000000..a1e353da2 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CommonViewportDataSource.swift @@ -0,0 +1,80 @@ +import Combine +import CoreLocation +import MapboxDirections +import MapboxMaps +import Turf +import UIKit + +@MainActor +class CommonViewportDataSource { + var navigationCameraOptions: AnyPublisher { + _navigationCameraOptions.eraseToAnyPublisher() + } + + private var _navigationCameraOptions: CurrentValueSubject = .init(.init()) + + var currentNavigationCameraOptions: NavigationCameraOptions { + get { + _navigationCameraOptions.value + } + + set { + _navigationCameraOptions.value = newValue + } + } + + var options: NavigationViewportDataSourceOptions = .init() + + weak var mapView: MapView? + + private var lifetimeSubscriptions: Set = [] + + private let viewportParametersProvider: ViewportParametersProvider + + private var previousViewportParameters: ViewportDataSourceState? + private var workQueue: DispatchQueue = .init( + label: "com.mapbox.navigation.camera", + qos: .userInteractive, + autoreleaseFrequency: .workItem + ) + + // MARK: Initializer Methods + + required init(_ mapView: MapView) { + self.mapView = mapView + self.viewportParametersProvider = .init() + } + + func update( + using viewportState: ViewportState, + updateClosure: @escaping (ViewportDataSourceState) -> NavigationCameraOptions? + ) { + Task { @MainActor [weak self] in + guard let self else { return } + let viewportParameters = await viewportParameters(with: viewportState) + guard viewportParameters != previousViewportParameters else { return } + + previousViewportParameters = viewportParameters + if let newOptions = updateClosure(viewportParameters) { + _navigationCameraOptions.send(newOptions) + } + } + } + + private func viewportParameters(with viewportState: ViewportState) async -> ViewportDataSourceState { + await withUnsafeContinuation { continuation in + let options = options + let provider = viewportParametersProvider + workQueue.async { + let parameters = provider.parameters( + with: viewportState.location, + heading: viewportState.heading, + routeProgress: viewportState.routeProgress, + viewportPadding: viewportState.viewportPadding, + options: options + ) + continuation.resume(returning: parameters) + } + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/MobileViewportDataSource.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/MobileViewportDataSource.swift new file mode 100644 index 000000000..ac00fa6d0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/MobileViewportDataSource.swift @@ -0,0 +1,341 @@ +import Combine +import CoreLocation +import MapboxDirections +import MapboxMaps +import Turf +import UIKit + +/// The class, which conforms to ``ViewportDataSource`` protocol and provides default implementation of it for iOS. +@MainActor +public class MobileViewportDataSource: ViewportDataSource { + private var commonDataSource: CommonViewportDataSource + + weak var mapView: MapView? + + /// Initializes ``MobileViewportDataSource`` instance. + /// - Parameter mapView: An instance of `MapView`, which is going to be used for viewport calculation. `MapView` + /// will be weakly stored by ``CarPlayViewportDataSource``. + public required init(_ mapView: MapView) { + self.mapView = mapView + self.commonDataSource = .init(mapView) + } + + /// Options, which give the ability to control whether certain `CameraOptions` will be generated. + public var options: NavigationViewportDataSourceOptions { + get { commonDataSource.options } + set { commonDataSource.options = newValue } + } + + /// Notifies that the navigation camera options have changed in response to a viewport change or a manual + /// change via ``currentNavigationCameraOptions``. + public var navigationCameraOptions: AnyPublisher { + commonDataSource.navigationCameraOptions + } + + /// The last calculated or set manually ``NavigationCameraOptions``. + /// + /// You can disable calculation of specific properties by changing ``options`` and setting a desired value directly. + /// + /// For example, setting a zoom level manually for the following camera state will require: + /// 1. Setting ``FollowingCameraOptions/zoomUpdatesAllowed`` to `false`. + /// 2. Updating `zoom` of `CameraOptions` to a desired value. + /// + /// > Important: If you don't disable calculation, the value that is set manually will be overriden. + public var currentNavigationCameraOptions: NavigationCameraOptions { + get { + commonDataSource.currentNavigationCameraOptions + } + + set { + commonDataSource.currentNavigationCameraOptions = newValue + } + } + + /// Updates ``NavigationCameraOptions`` accoridng to the navigation state. + /// - Parameters: + /// - viewportState: The current viewport state. + public func update(using viewportState: ViewportState) { + commonDataSource.update(using: viewportState) { [weak self] state in + guard let self else { return nil } + return NavigationCameraOptions( + followingCamera: newFollowingCamera(with: state), + overviewCamera: newOverviewCamera(with: state) + ) + } + } + + private func newFollowingCamera(with state: ViewportDataSourceState) -> CameraOptions { + guard let mapView else { return .init() } + + let followingCameraOptions = options.followingCameraOptions + let viewportPadding = state.viewportPadding + var newOptions = currentNavigationCameraOptions.followingCamera + + if let location = state.location, state.navigationState.isInPassiveNavigationOrCompletedActive { + if followingCameraOptions.centerUpdatesAllowed || followingCamera.center == nil { + newOptions.center = location.coordinate + } + + if followingCameraOptions.zoomUpdatesAllowed || followingCamera.zoom == nil { + let altitude = 1700.0 + let zoom = CGFloat(ZoomLevelForAltitude( + altitude, + mapView.mapboxMap.cameraState.pitch, + location.coordinate.latitude, + mapView.bounds.size + )) + + newOptions.zoom = zoom + } + + if followingCameraOptions.bearingUpdatesAllowed || followingCamera.bearing == nil { + if followingCameraOptions.followsLocationCourse { + newOptions.bearing = location.course + } else { + newOptions.bearing = 0.0 + } + } + + newOptions.anchor = mapView.center + + if followingCameraOptions.pitchUpdatesAllowed || followingCamera.pitch == nil { + newOptions.pitch = 0.0 + } + + if followingCameraOptions.paddingUpdatesAllowed || followingCamera.padding == nil { + newOptions.padding = mapView.safeAreaInsets + } + + return newOptions + } + + if let location = state.location, case .active(let activeState) = state.navigationState, + !activeState.isRouteComplete + { + let coordinatesToManeuver = activeState.coordinatesToManeuver + let lookaheadDistance = activeState.lookaheadDistance + + var compoundManeuvers: [[CLLocationCoordinate2D]] = [] + let geometryFramingAfterManeuver = followingCameraOptions.geometryFramingAfterManeuver + let pitchСoefficient = pitchСoefficient( + distanceRemainingOnStep: activeState.distanceRemainingOnStep, + currentCoordinate: location.coordinate, + currentLegStepIndex: activeState.currentLegStepIndex, + currentLegSteps: activeState.currentLegSteps + ) + let pitch = followingCameraOptions.defaultPitch * pitchСoefficient + + if geometryFramingAfterManeuver.enabled { + let nextStepIndex = min(activeState.currentLegStepIndex + 1, activeState.currentLegSteps.count - 1) + + var totalDistance: CLLocationDistance = 0.0 + for (index, step) in activeState.currentLegSteps.dropFirst(nextStepIndex).enumerated() { + guard let stepCoordinates = step.shape?.coordinates, + let distance = stepCoordinates.distance() else { continue } + + if index == 0 { + if distance >= geometryFramingAfterManeuver.distanceToFrameAfterManeuver { + let trimmedStepCoordinates = stepCoordinates + .trimmed(distance: geometryFramingAfterManeuver.distanceToFrameAfterManeuver) + compoundManeuvers.append(trimmedStepCoordinates) + break + } else { + compoundManeuvers.append(stepCoordinates) + totalDistance += distance + } + } else if distance >= 0.0, totalDistance < geometryFramingAfterManeuver + .distanceToCoalesceCompoundManeuvers + { + if distance + totalDistance >= geometryFramingAfterManeuver + .distanceToCoalesceCompoundManeuvers + { + let remanentDistance = geometryFramingAfterManeuver + .distanceToCoalesceCompoundManeuvers - totalDistance + let trimmedStepCoordinates = stepCoordinates.trimmed(distance: remanentDistance) + compoundManeuvers.append(trimmedStepCoordinates) + break + } else { + compoundManeuvers.append(stepCoordinates) + totalDistance += distance + } + } + } + } + + let coordinatesForManeuverFraming = compoundManeuvers.reduce([], +) + var coordinatesToFrame = coordinatesToManeuver.sliced( + from: nil, + to: LineString(coordinatesToManeuver).coordinateFromStart(distance: lookaheadDistance) + ) + let pitchNearManeuver = followingCameraOptions.pitchNearManeuver + if pitchNearManeuver.enabled, + activeState.distanceRemainingOnStep <= pitchNearManeuver.triggerDistanceToManeuver + { + coordinatesToFrame += coordinatesForManeuverFraming + } + + if options.followingCameraOptions.centerUpdatesAllowed || followingCamera.center == nil { + var center = location.coordinate + if let boundingBox = BoundingBox(from: coordinatesToFrame) { + let coordinates = [ + center, + [boundingBox.northEast, boundingBox.southWest].centerCoordinate, + ] + + let centerLineString = LineString(coordinates) + let centerLineStringTotalDistance = centerLineString.distance() ?? 0.0 + let centerCoordDistance = centerLineStringTotalDistance * (1 - pitchСoefficient) + if let adjustedCenter = centerLineString.coordinateFromStart(distance: centerCoordDistance) { + center = adjustedCenter + } + } + + newOptions.center = center + } + + if options.followingCameraOptions.zoomUpdatesAllowed || followingCamera.zoom == nil { + let defaultZoomLevel = 12.0 + let followingMobileCameraZoom = zoom( + coordinatesToFrame, + mapView: mapView, + pitch: pitch, + maxPitch: followingCameraOptions.defaultPitch, + edgeInsets: viewportPadding, + defaultZoomLevel: defaultZoomLevel, + maxZoomLevel: followingCameraOptions.zoomRange.upperBound, + minZoomLevel: followingCameraOptions.zoomRange.lowerBound + ) + + newOptions.zoom = followingMobileCameraZoom + } + + if options.followingCameraOptions.bearingUpdatesAllowed || followingCamera.bearing == nil { + var bearing = location.course + let distance = fmax( + lookaheadDistance, + geometryFramingAfterManeuver.enabled + ? geometryFramingAfterManeuver.distanceToCoalesceCompoundManeuvers + : 0.0 + ) + let coordinatesForIntersections = coordinatesToManeuver.sliced( + from: nil, + to: LineString(coordinatesToManeuver) + .coordinateFromStart(distance: distance) + ) + + bearing = self.bearing( + location.course, + mapView: mapView, + coordinatesToManeuver: coordinatesForIntersections + ) + + var headingDirection: CLLocationDirection? + let isWalking = activeState.transportType == .walking + if isWalking { + if let trueHeading = state.heading?.trueHeading, trueHeading >= 0 { + headingDirection = trueHeading + } else if let magneticHeading = state.heading?.magneticHeading, magneticHeading >= 0 { + headingDirection = magneticHeading + } else { + headingDirection = bearing + } + } + + newOptions.bearing = !isWalking ? bearing : headingDirection + } + + let followingMobileCameraAnchor = anchor( + pitchСoefficient, + bounds: mapView.bounds, + edgeInsets: viewportPadding + ) + + newOptions.anchor = followingMobileCameraAnchor + + if options.followingCameraOptions.pitchUpdatesAllowed || followingCamera.pitch == nil { + newOptions.pitch = CGFloat(pitch) + } + + if options.followingCameraOptions.paddingUpdatesAllowed || followingCamera.padding == nil { + newOptions.padding = UIEdgeInsets( + top: followingMobileCameraAnchor.y, + left: viewportPadding.left, + bottom: mapView.bounds.height - followingMobileCameraAnchor + .y + 1.0, + right: viewportPadding.right + ) + } + } + return newOptions + } + + private func newOverviewCamera(with state: ViewportDataSourceState) -> CameraOptions { + guard let mapView else { return .init() } + + // In active guidance navigation, camera in overview mode is relevant, during free-drive + // navigation it's not used. + guard case .active(let activeState) = state.navigationState else { return overviewCamera } + + var newOptions = currentNavigationCameraOptions.overviewCamera + let remainingCoordinatesOnRoute = activeState.remainingCoordinatesOnRoute + let viewportPadding = state.viewportPadding + + let overviewCameraOptions = options.overviewCameraOptions + + if overviewCameraOptions.pitchUpdatesAllowed || overviewCamera.pitch == nil { + newOptions.pitch = 0.0 + } + + if overviewCameraOptions.centerUpdatesAllowed || overviewCamera.center == nil { + if let boundingBox = BoundingBox(from: remainingCoordinatesOnRoute) { + let center = [ + boundingBox.southWest, + boundingBox.northEast, + ].centerCoordinate + + newOptions.center = center + } + } + + newOptions.anchor = anchor( + bounds: mapView.bounds, + edgeInsets: viewportPadding + ) + + if overviewCameraOptions.bearingUpdatesAllowed || overviewCamera.bearing == nil { + // In case if `NavigationCamera` is already in ``NavigationCameraState/overview`` value + // of bearing will be also ignored. + let bearing = 0.0 + + var headingDirection: CLLocationDirection? + let isWalking = activeState.transportType == .walking + if isWalking { + if let trueHeading = state.heading?.trueHeading, trueHeading >= 0 { + headingDirection = trueHeading + } else if let magneticHeading = state.heading?.magneticHeading, magneticHeading >= 0 { + headingDirection = magneticHeading + } else { + headingDirection = bearing + } + } + + newOptions.bearing = !isWalking ? bearing : headingDirection + } + + if overviewCameraOptions.zoomUpdatesAllowed || overviewCamera.zoom == nil { + newOptions.zoom = overviewCameraZoom( + remainingCoordinatesOnRoute, + mapView: mapView, + pitch: newOptions.pitch, + bearing: newOptions.bearing, + edgeInsets: viewportPadding, + maxZoomLevel: overviewCameraOptions.maximumZoomLevel + ) + } + + if overviewCameraOptions.paddingUpdatesAllowed || overviewCamera.padding == nil { + newOptions.padding = viewportPadding + } + return newOptions + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource+Calculation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource+Calculation.swift new file mode 100644 index 000000000..f7b99d289 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource+Calculation.swift @@ -0,0 +1,146 @@ +import CoreLocation +import MapboxDirections +import MapboxMaps +import Turf +import UIKit + +extension ViewportDataSource { + func bearing( + _ initialBearing: CLLocationDirection, + mapView: MapView?, + coordinatesToManeuver: [CLLocationCoordinate2D]? = nil + ) -> CLLocationDirection { + var bearing = initialBearing + + if let coordinates = coordinatesToManeuver, + let firstCoordinate = coordinates.first, + let lastCoordinate = coordinates.last + { + let directionToManeuver = firstCoordinate.direction(to: lastCoordinate) + let directionDiff = directionToManeuver.shortestRotation(angle: initialBearing) + let bearingSmoothing = options.followingCameraOptions.bearingSmoothing + let bearingMaxDiff = bearingSmoothing.enabled ? bearingSmoothing.maximumBearingSmoothingAngle : 0.0 + if fabs(directionDiff) > bearingMaxDiff { + bearing += bearingMaxDiff * (directionDiff < 0.0 ? -1.0 : 1.0) + } else { + bearing = firstCoordinate.direction(to: lastCoordinate) + } + } + + let mapViewBearing = Double(mapView?.mapboxMap.cameraState.bearing ?? 0.0) + return mapViewBearing + bearing.shortestRotation(angle: mapViewBearing) + } + + func zoom( + _ coordinates: [CLLocationCoordinate2D], + mapView: MapView?, + pitch: Double = 0.0, + maxPitch: Double = 0.0, + edgeInsets: UIEdgeInsets = .zero, + defaultZoomLevel: Double = 12.0, + maxZoomLevel: Double = 22.0, + minZoomLevel: Double = 2.0 + ) -> CGFloat { + guard let mapView, + let boundingBox = BoundingBox(from: coordinates) else { return CGFloat(defaultZoomLevel) } + + let mapViewInsetWidth = mapView.bounds.size.width - edgeInsets.left - edgeInsets.right + let mapViewInsetHeight = mapView.bounds.size.height - edgeInsets.top - edgeInsets.bottom + let widthDelta = mapViewInsetHeight * 2 - mapViewInsetWidth + let pitchDelta = CGFloat(pitch / maxPitch) * widthDelta + let widthWithPitchEffect = CGFloat(mapViewInsetWidth + CGFloat(pitchDelta.isNaN ? 0.0 : pitchDelta)) + let heightWithPitchEffect = + CGFloat(mapViewInsetHeight + mapViewInsetHeight * CGFloat(sin(pitch * .pi / 180.0)) * 1.25) + let zoomLevel = boundingBox.zoomLevel(fitTo: CGSize(width: widthWithPitchEffect, height: heightWithPitchEffect)) + + return CGFloat(max(min(zoomLevel, maxZoomLevel), minZoomLevel)) + } + + func overviewCameraZoom( + _ coordinates: [CLLocationCoordinate2D], + mapView: MapView?, + pitch: CGFloat?, + bearing: CLLocationDirection?, + edgeInsets: UIEdgeInsets, + defaultZoomLevel: Double = 12.0, + maxZoomLevel: Double = 22.0, + minZoomLevel: Double = 2.0 + ) -> CGFloat { + guard let mapView else { return CGFloat(defaultZoomLevel) } + + let initialCameraOptions = CameraOptions( + padding: edgeInsets, + bearing: 0, + pitch: 0 + ) + guard let options = try? mapView.mapboxMap.camera( + for: coordinates, + camera: initialCameraOptions, + coordinatesPadding: nil, + maxZoom: nil, + offset: nil + ) else { + return CGFloat(defaultZoomLevel) + } + return CGFloat(max(min(options.zoom ?? defaultZoomLevel, maxZoomLevel), minZoomLevel)) + } + + func anchor( + _ pitchСoefficient: Double = 0.0, + bounds: CGRect = .zero, + edgeInsets: UIEdgeInsets = .zero + ) -> CGPoint { + let xCenter = max(((bounds.size.width - edgeInsets.left - edgeInsets.right) / 2.0) + edgeInsets.left, 0.0) + let height = (bounds.size.height - edgeInsets.top - edgeInsets.bottom) + let yCenter = max((height / 2.0) + edgeInsets.top, 0.0) + let yOffsetCenter = max((height / 2.0) - 7.0, 0.0) * CGFloat(pitchСoefficient) + yCenter + + return CGPoint(x: xCenter, y: yOffsetCenter) + } + + func pitchСoefficient( + distanceRemainingOnStep: CLLocationDistance, + currentCoordinate: CLLocationCoordinate2D, + currentLegStepIndex: Int, + currentLegSteps: [RouteStep] + ) -> Double { + let defaultPitchСoefficient = 1.0 + let pitchNearManeuver = options.followingCameraOptions.pitchNearManeuver + guard pitchNearManeuver.enabled else { return defaultPitchСoefficient } + + var shouldIgnoreManeuver = false + if let upcomingStep = currentLegSteps[safe: currentLegStepIndex + 1] { + if currentLegStepIndex == currentLegSteps.count - 2 { + shouldIgnoreManeuver = true + } + + let maneuvers: [ManeuverType] = [.continue, .merge, .takeOnRamp, .takeOffRamp, .reachFork] + if maneuvers.contains(upcomingStep.maneuverType) { + shouldIgnoreManeuver = true + } + } + + if distanceRemainingOnStep <= pitchNearManeuver.triggerDistanceToManeuver, !shouldIgnoreManeuver, + pitchNearManeuver.triggerDistanceToManeuver != 0.0 + { + return distanceRemainingOnStep / pitchNearManeuver.triggerDistanceToManeuver + } + return defaultPitchСoefficient + } + + var followingCamera: CameraOptions { + currentNavigationCameraOptions.followingCamera + } + + var overviewCamera: CameraOptions { + currentNavigationCameraOptions.overviewCamera + } +} + +extension ViewportDataSourceState.NavigationState { + var isInPassiveNavigationOrCompletedActive: Bool { + if case .passive = self { return true } + if case .active(let activeState) = self, activeState.isRouteComplete { return true } + return false + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource.swift new file mode 100644 index 000000000..9274ee980 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource.swift @@ -0,0 +1,58 @@ +import Combine +import CoreLocation +import MapboxMaps +import UIKit + +/// Represents the state of the viewport. +public struct ViewportState: Equatable, Sendable { + /// The current location of the user. + public let location: CLLocation + /// The navigation route progress. + public let routeProgress: RouteProgress? + /// The padding applied to the viewport. + public let viewportPadding: UIEdgeInsets + /// The current user heading. + public let heading: CLHeading? + + /// Initializes a new ``ViewportState`` instance. + /// - Parameters: + /// - location: The current location of the user. + /// - routeProgress: The navigation route progress. Pass `nil` in case of no active navigation at the moment. + /// - viewportPadding: The padding applied to the viewport. + /// - heading: The current user heading. + public init( + location: CLLocation, + routeProgress: RouteProgress?, + viewportPadding: UIEdgeInsets, + heading: CLHeading? + ) { + self.location = location + self.routeProgress = routeProgress + self.viewportPadding = viewportPadding + self.heading = heading + } +} + +/// The protocol, which is used to fill and store ``NavigationCameraOptions`` which will be used by ``NavigationCamera`` +/// for execution of transitions and continuous updates. +/// +/// By default Navigation SDK for iOS provides default implementation of ``ViewportDataSource`` in +/// ``MobileViewportDataSource`` and ``CarPlayViewportDataSource``. +@MainActor +public protocol ViewportDataSource: AnyObject { + /// Options, which give the ability to control whether certain `CameraOptions` will be generated. + var options: NavigationViewportDataSourceOptions { get } + + /// Notifies that the navigation camera options have changed in response to a viewport change. + var navigationCameraOptions: AnyPublisher { get } + + /// The last calculated ``NavigationCameraOptions``. + var currentNavigationCameraOptions: NavigationCameraOptions { get } + + /// Updates ``NavigationCameraOptions`` accoridng to the navigation state. + /// - Parameters: + /// - viewportState: The current viewport state. + func update(using viewportState: ViewportState) +} + +extension CLHeading: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSourceState.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSourceState.swift new file mode 100644 index 000000000..40e34cb2a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSourceState.swift @@ -0,0 +1,28 @@ +import CoreLocation +import Foundation +import MapboxDirections +import Turf +import UIKit + +struct ViewportDataSourceState: Equatable, Sendable { + enum NavigationState: Equatable, Sendable { + case passive + case active(ActiveNavigationState) + } + + struct ActiveNavigationState: Equatable, Sendable { + var coordinatesToManeuver: [LocationCoordinate2D] + var lookaheadDistance: LocationDistance + var currentLegStepIndex: Int + var currentLegSteps: [RouteStep] + var isRouteComplete: Bool + var remainingCoordinatesOnRoute: [LocationCoordinate2D] + var transportType: TransportType + var distanceRemainingOnStep: CLLocationDistance + } + + var location: CLLocation? + var heading: CLHeading? + var navigationState: NavigationState + var viewportPadding: UIEdgeInsets +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportParametersProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportParametersProvider.swift new file mode 100644 index 000000000..869957c15 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportParametersProvider.swift @@ -0,0 +1,100 @@ +import CoreLocation +import Foundation +import MapboxDirections +import UIKit + +struct ViewportParametersProvider: Sendable { + func parameters( + with location: CLLocation?, + heading: CLHeading?, + routeProgress: RouteProgress?, + viewportPadding: UIEdgeInsets, + options: NavigationViewportDataSourceOptions + ) -> ViewportDataSourceState { + if let routeProgress { + let intersectionDensity = options.followingCameraOptions.intersectionDensity + let stepIndex = routeProgress.currentLegProgress.stepIndex + let nextStepIndex = min(stepIndex + 1, routeProgress.currentLeg.steps.count - 1) + + var remainingCoordinatesOnRoute = routeProgress.currentLegProgress.currentStepProgress + .remainingStepCoordinates() + routeProgress.currentLeg.steps[nextStepIndex...] + .lazy + .compactMap { $0.shape?.coordinates } + .forEach { stepCoordinates in + remainingCoordinatesOnRoute.append(contentsOf: stepCoordinates) + } + + return .init( + location: location, + heading: heading, + navigationState: .active( + .init( + coordinatesToManeuver: routeProgress.currentLegProgress.currentStepProgress + .remainingStepCoordinates(), + lookaheadDistance: lookaheadDistance(routeProgress, intersectionDensity: intersectionDensity), + currentLegStepIndex: routeProgress.currentLegProgress.stepIndex, + currentLegSteps: routeProgress.currentLeg.steps, + isRouteComplete: routeProgress.routeIsComplete == true, + remainingCoordinatesOnRoute: remainingCoordinatesOnRoute, + transportType: routeProgress.currentLegProgress.currentStep.transportType, + distanceRemainingOnStep: routeProgress.currentLegProgress.currentStepProgress.distanceRemaining + ) + ), + viewportPadding: viewportPadding + ) + } else { + return .init( + location: location, + navigationState: .passive, + viewportPadding: viewportPadding + ) + } + } + + /// Calculates lookahead distance based on current ``RouteProgress`` and ``IntersectionDensity`` coefficients. + /// Lookahead distance value will be influenced by both ``IntersectionDensity.minimumDistanceBetweenIntersections`` + /// and ``IntersectionDensity.averageDistanceMultiplier``. + /// - Parameters: + /// - routeProgress: Current `RouteProgress` + /// - intersectionDensity: Lookahead distance + /// - Returns: The lookahead distance. + private func lookaheadDistance( + _ routeProgress: RouteProgress, + intersectionDensity: IntersectionDensity + ) -> CLLocationDistance { + let averageIntersectionDistances = routeProgress.route.legs.map { leg -> [CLLocationDistance] in + return leg.steps.map { step -> CLLocationDistance in + if let firstStepCoordinate = step.shape?.coordinates.first, + let lastStepCoordinate = step.shape?.coordinates.last + { + let intersectionLocations = [firstStepCoordinate] + ( + step.intersections?.map(\.location) ?? [] + ) + + [lastStepCoordinate] + let intersectionDistances = intersectionLocations[1...].enumerated() + .map { index, intersection -> CLLocationDistance in + return intersection.distance(to: intersectionLocations[index]) + } + let filteredIntersectionDistances = intersectionDensity.enabled + ? intersectionDistances.filter { $0 > intersectionDensity.minimumDistanceBetweenIntersections } + : intersectionDistances + let averageIntersectionDistance = filteredIntersectionDistances + .reduce(0.0, +) / Double(filteredIntersectionDistances.count) + return averageIntersectionDistance + } + + return 0.0 + } + } + + let averageDistanceMultiplier = intersectionDensity.enabled ? intersectionDensity + .averageDistanceMultiplier : 1.0 + let currentRouteLegIndex = routeProgress.legIndex + let currentRouteStepIndex = routeProgress.currentLegProgress.stepIndex + let lookaheadDistance = averageIntersectionDistances[currentRouteLegIndex][currentRouteStepIndex] * + averageDistanceMultiplier + + return lookaheadDistance + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/MapPoint.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/MapPoint.swift new file mode 100644 index 000000000..6b06d0afb --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/MapPoint.swift @@ -0,0 +1,16 @@ +import CoreLocation + +/// Represents a point that user tapped on the map. +public struct MapPoint: Equatable, Sendable { + /// Name of the POI that user tapped on. Can be `nil` if there were no POIs nearby. + /// Developers can adjust ``NavigationMapView/poiClickableAreaSize`` + /// to increase the search area around the touch point. + public let name: String? + /// Coordinate of user's tap. + public let coordinate: CLLocationCoordinate2D + + public init(name: String?, coordinate: CLLocationCoordinate2D) { + self.name = name + self.coordinate = coordinate + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/MapView.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/MapView.swift new file mode 100644 index 000000000..622273797 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/MapView.swift @@ -0,0 +1,205 @@ +import CoreLocation +import Foundation +import MapboxDirections +import MapboxMaps + +private let trafficTileSetIdentifiers = Set([ + "mapbox.mapbox-traffic-v1", + "mapbox.mapbox-traffic-v2-beta", +]) + +private let incidentsTileSetIdentifiers = Set([ + "mapbox.mapbox-incidents-v1", + "mapbox.mapbox-incidents-v2-beta", +]) + +/// An extension on `MapView` that allows for toggling traffic on a map style that contains a [Mapbox Traffic +/// source](https://docs.mapbox.com/vector-tiles/mapbox-traffic-v1/). +extension MapView { + /// Returns a set of tile set identifiers for specific `sourceIdentifier`. + /// + /// - parameter sourceIdentifier: Identifier of the source, which will be searched for in current style of the + /// ///`MapView`. + /// - returns: Set of tile set identifiers. + func tileSetIdentifiers(_ sourceIdentifier: String) -> Set { + if let properties = try? mapboxMap.sourceProperties(for: sourceIdentifier), + let url = properties["url"] as? String, + let configurationURL = URL(string: url), + configurationURL.scheme == "mapbox", + let tileSetIdentifiers = configurationURL.host?.components(separatedBy: ",") + { + return Set(tileSetIdentifiers) + } + + return Set() + } + + /// Returns a list of identifiers of the tile sets that make up specific source type. + /// + /// This array contains multiple entries for a composited source. This property is empty for non-Mapbox-hosted tile + /// sets and sources with type other than `vector`. + /// + /// - parameter sourceIdentifier: Identifier of the source. + /// - parameter sourceType: Type of the source (e.g. `vector`). + /// - returns: List of tile set identifiers. + func tileSetIdentifiers(_ sourceIdentifier: String, sourceType: String) -> [String] { + if sourceType == "vector" { + return Array(tileSetIdentifiers(sourceIdentifier)) + } + + return [] + } + + /// Returns a set of source identifiers for tilesets that are or include the given source. + /// + /// - parameter tileSetIdentifier: Identifier of the tile set in the form `user.tileset`. + /// - returns: Set of source identifiers. + func sourceIdentifiers(_ tileSetIdentifiers: Set) -> Set { + return Set(mapboxMap.allSourceIdentifiers.filter { + $0.type.rawValue == "vector" + }.filter { + !self.tileSetIdentifiers($0.id).isDisjoint(with: tileSetIdentifiers) + }.map(\.id)) + } + + /// Returns a Boolean value indicating whether data from the given tile set layers is currently all visible in the + /// map view’s style. + /// + /// - parameter tileSetIdentifiers: Identifiers of the tile sets in the form `user.tileset`. + /// - parameter layerIdentifier: Identifier of the layer in the tile set; in other words, a source layer identifier. + /// Not to be confused with a style layer. + func showsTileSet(with tileSetIdentifiers: Set, layerIdentifier: String) -> Bool { + let sourceIdentifiers = sourceIdentifiers(tileSetIdentifiers) + var foundTileSets = false + + for mapViewLayerIdentifier in mapboxMap.allLayerIdentifiers.map(\.id) { + guard let sourceIdentifier = mapboxMap.layerProperty( + for: mapViewLayerIdentifier, + property: "source" + ).value as? String, + let sourceLayerIdentifier = mapboxMap.layerProperty( + for: mapViewLayerIdentifier, + property: "source-layer" + ).value as? String + else { return false } + + if sourceIdentifiers.contains(sourceIdentifier), sourceLayerIdentifier == layerIdentifier { + foundTileSets = true + let visibility = mapboxMap.layerProperty(for: mapViewLayerIdentifier, property: "visibility") + .value as? String + if visibility != "visible" { + return false + } + } + } + + return foundTileSets + } + + /// Shows or hides data from the given tile set layers. + /// + /// - parameter isVisible: Parameter, which controls whether layer should be visible or not. + /// - parameter tileSetIdentifiers: Identifiers of the tile sets in the form `user.tileset`. + /// - parameter layerIdentifier: Identifier of the layer in the tile set; in other words, a source layer identifier. + /// Not to be confused with a style layer. + func setShowsTileSet(_ isVisible: Bool, with tileSetIdentifiers: Set, layerIdentifier: String) { + let sourceIdentifiers = sourceIdentifiers(tileSetIdentifiers) + + for mapViewLayerIdentifier in mapboxMap.allLayerIdentifiers.map(\.id) { + guard let sourceIdentifier = mapboxMap.layerProperty( + for: mapViewLayerIdentifier, + property: "source" + ).value as? String, + let sourceLayerIdentifier = mapboxMap.layerProperty( + for: mapViewLayerIdentifier, + property: "source-layer" + ).value as? String + else { return } + + if sourceIdentifiers.contains(sourceIdentifier), sourceLayerIdentifier == layerIdentifier { + let properties = [ + "visibility": isVisible ? "visible" : "none", + ] + try? mapboxMap.setLayerProperties(for: mapViewLayerIdentifier, properties: properties) + } + } + } + + /// A Boolean value indicating whether traffic congestion lines are visible in the map view’s style. + var showsTraffic: Bool { + get { + return showsTileSet(with: trafficTileSetIdentifiers, layerIdentifier: "traffic") + } + set { + setShowsTileSet(newValue, with: trafficTileSetIdentifiers, layerIdentifier: "traffic") + } + } + + /// A Boolean value indicating whether incidents, such as road closures and detours, are visible in the map view’s + /// style. + var showsIncidents: Bool { + get { + return showsTileSet(with: incidentsTileSetIdentifiers, layerIdentifier: "closures") + } + set { + setShowsTileSet(newValue, with: incidentsTileSetIdentifiers, layerIdentifier: "closures") + } + } + + /// Method, which returns list of source identifiers, which contain streets tile set. + func streetsSources() -> [SourceInfo] { + return mapboxMap.allSourceIdentifiers.filter { + let identifiers = tileSetIdentifiers($0.id, sourceType: $0.type.rawValue) + return VectorSource.isMapboxStreets(identifiers) + } + } + + /// Attempts to localize road labels into the local language and other labels into the given locale. + func localizeLabels(into locale: Locale) { + guard let mapboxStreetsSource = streetsSources().first else { return } + + let streetsSourceTilesetIdentifiers = tileSetIdentifiers(mapboxStreetsSource.id) + let roadLabelSourceLayerIdentifier = streetsSourceTilesetIdentifiers + .compactMap { VectorSource.roadLabelLayerIdentifiersByTileSetIdentifier[$0] + }.first + + let localizableLayerIdentifiers = mapboxMap.allLayerIdentifiers.lazy + .filter { + $0.type == .symbol + } + // We only know how to localize layers backed by the Mapbox Streets source. + .filter { + self.mapboxMap.layerProperty(for: $0.id, property: "source").value as? String == mapboxStreetsSource.id + } + // Road labels should match road signage, so they should not be localized. + // TODO: Actively delocalize road labels into the “name” property: https://github.com/mapbox/mapbox-maps-ios/issues/653 + .filter { + self.mapboxMap.layerProperty( + for: $0.id, + property: "source-layer" + ).value as? String != roadLabelSourceLayerIdentifier + } + .map(\.id) + try? mapboxMap.localizeLabels(into: locale, forLayerIds: Array(localizableLayerIdentifiers)) + } +} + +extension MapView { + /// Returns a tileset descriptor for current map style. + /// + /// - parameter zoomRange: Closed range zoom level for the tile package. + /// - returns: A tileset descriptor. + func tilesetDescriptor(zoomRange: ClosedRange) -> TilesetDescriptor? { + guard let styleURI = mapboxMap.styleURI, + URL(string: styleURI.rawValue)?.scheme == "mapbox" + else { return nil } + + let offlineManager = OfflineManager() + let tilesetDescriptorOptions = TilesetDescriptorOptions( + styleURI: styleURI, + zoomRange: zoomRange, + tilesets: nil + ) + return offlineManager.createTilesetDescriptor(for: tilesetDescriptorOptions) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+ContinuousAlternatives.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+ContinuousAlternatives.swift new file mode 100644 index 000000000..2a533cf93 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+ContinuousAlternatives.swift @@ -0,0 +1,64 @@ +import _MapboxNavigationHelpers +import Foundation +import MapboxDirections +import Turf +import UIKit + +extension NavigationMapView { + /// Returns a list of the ``AlternativeRoute``s, that are close to a certain point and are within threshold distance + /// defined in ``NavigationMapView/tapGestureDistanceThreshold``. + /// + /// - parameter point: Point on the screen. + /// - returns: List of the alternative routes, which were found. If there are no continuous alternatives routes on + /// the map view `nil` will be returned. + /// An empty array is returned if no alternative route was tapped or if there are multiple equally fitting + /// routes at the tap coordinate. + func continuousAlternativeRoutes(closeTo point: CGPoint) -> [AlternativeRoute]? { + guard let routes, !routes.alternativeRoutes.isEmpty + else { + return nil + } + + // Workaround for XCode 12.5 compilation bug + typealias RouteWithMetadata = (route: Route, index: Int, distance: LocationDistance) + + let continuousAlternatives = routes.alternativeRoutes + // Add the main route to detect if the main route is the closest to the point. The main route is excluded from + // the result array. + let allRoutes = [routes.mainRoute.route] + continuousAlternatives.map { $0.route } + + // Filter routes with at least 2 coordinates and within tap distance. + let tapCoordinate = mapView.mapboxMap.coordinate(for: point) + let routeMetadata = allRoutes.enumerated() + .compactMap { index, route -> RouteWithMetadata? in + guard route.shape?.coordinates.count ?? 0 > 1 else { + return nil + } + guard let closestCoordinate = route.shape?.closestCoordinate(to: tapCoordinate)?.coordinate else { + return nil + } + + let closestPoint = mapView.mapboxMap.point(for: closestCoordinate) + guard closestPoint.distance(to: point) < tapGestureDistanceThreshold else { + return nil + } + let distance = closestCoordinate.distance(to: tapCoordinate) + return RouteWithMetadata(route: route, index: index, distance: distance) + } + + // Sort routes by closest distance to tap gesture. + let closest = routeMetadata.sorted { (lhs: RouteWithMetadata, rhs: RouteWithMetadata) -> Bool in + return lhs.distance < rhs.distance + } + + // Exclude the routes if the distance is the same and we cannot distinguish the routes. + if routeMetadata.count > 1, abs(routeMetadata[0].distance - routeMetadata[1].distance) < 1e-6 { + return [] + } + + return closest.compactMap { (item: RouteWithMetadata) -> AlternativeRoute? in + guard item.index > 0 else { return nil } + return continuousAlternatives[item.index - 1] + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+Gestures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+Gestures.swift new file mode 100644 index 000000000..955badd75 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+Gestures.swift @@ -0,0 +1,206 @@ +import _MapboxNavigationHelpers +import Foundation +import MapboxDirections +import MapboxMaps +import Turf +import UIKit + +extension NavigationMapView { + func setupGestureRecognizers() { + // Gesture recognizer, which is used to detect long taps on any point on the map. + let longPressGestureRecognizer = UILongPressGestureRecognizer( + target: self, + action: #selector(handleLongPress(_:)) + ) + addGestureRecognizer(longPressGestureRecognizer) + + // Gesture recognizer, which is used to detect taps on route line, waypoint or POI + mapViewTapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(didReceiveTap(gesture:)) + ) + mapViewTapGestureRecognizer.delegate = self + mapView.addGestureRecognizer(mapViewTapGestureRecognizer) + + makeGestureRecognizersDisableCameraFollowing() + makeTapGestureRecognizerStopAnimatedTransitions() + } + + @objc + private func handleLongPress(_ gesture: UIGestureRecognizer) { + guard gesture.state == .began else { return } + let gestureLocation = gesture.location(in: self) + Task { @MainActor in + let point = await mapPoint(at: gestureLocation) + delegate?.navigationMapView(self, userDidLongTap: point) + } + } + + /// Modifies `MapView` gesture recognizers to disable follow mode and move `NavigationCamera` to + /// `NavigationCameraState.idle` state. + private func makeGestureRecognizersDisableCameraFollowing() { + for gestureRecognizer in mapView.gestureRecognizers ?? [] + where gestureRecognizer is UIPanGestureRecognizer + || gestureRecognizer is UIRotationGestureRecognizer + || gestureRecognizer is UIPinchGestureRecognizer + || gestureRecognizer == mapView.gestures.doubleTapToZoomInGestureRecognizer + || gestureRecognizer == mapView.gestures.doubleTouchToZoomOutGestureRecognizer + + { + gestureRecognizer.addTarget(self, action: #selector(switchToIdleCamera)) + } + } + + private func makeTapGestureRecognizerStopAnimatedTransitions() { + for gestureRecognizer in mapView.gestureRecognizers ?? [] + where gestureRecognizer is UITapGestureRecognizer + && gestureRecognizer != mapView.gestures.doubleTouchToZoomOutGestureRecognizer + { + gestureRecognizer.addTarget(self, action: #selector(switchToIdleCameraIfNotFollowing)) + } + } + + @objc + private func switchToIdleCamera() { + update(navigationCameraState: .idle) + } + + @objc + private func switchToIdleCameraIfNotFollowing() { + guard navigationCamera.currentCameraState != .following else { return } + update(navigationCameraState: .idle) + } + + /// Fired when NavigationMapView detects a tap not handled elsewhere by other gesture recognizers. + @objc + private func didReceiveTap(gesture: UITapGestureRecognizer) { + guard gesture.state == .recognized else { return } + let tapPoint = gesture.location(in: mapView) + + Task { + if let allRoutes = routes?.allRoutes() { + let waypointTest = legSeparatingWaypoints(on: allRoutes, closeTo: tapPoint) + if let selected = waypointTest?.first { + delegate?.navigationMapView(self, didSelect: selected) + return + } + } + + if let alternativeRoute = continuousAlternativeRoutes(closeTo: tapPoint)?.first { + delegate?.navigationMapView(self, didSelect: alternativeRoute) + return + } + + let point = await mapPoint(at: tapPoint) + + if point.name != nil { + delegate?.navigationMapView(self, userDidTap: point) + } + } + } + + func legSeparatingWaypoints(on routes: [Route], closeTo point: CGPoint) -> [Waypoint]? { + // In case if route does not contain more than one leg - do nothing. + let multipointRoutes = routes.filter { $0.legs.count > 1 } + guard multipointRoutes.count > 0 else { return nil } + + let waypoints = multipointRoutes.compactMap { route in + route.legs.dropLast().compactMap { $0.destination } + }.flatMap { $0 } + + // Sort the array in order of closest to tap. + let tapCoordinate = mapView.mapboxMap.coordinate(for: point) + let closest = waypoints.sorted { left, right -> Bool in + let leftDistance = left.coordinate.projectedDistance(to: tapCoordinate) + let rightDistance = right.coordinate.projectedDistance(to: tapCoordinate) + return leftDistance < rightDistance + } + + // Filter to see which ones are under threshold. + let candidates = closest.filter { + let coordinatePoint = mapView.mapboxMap.point(for: $0.coordinate) + + return coordinatePoint.distance(to: point) < tapGestureDistanceThreshold + } + + return candidates + } + + private func mapPoint(at point: CGPoint) async -> MapPoint { + let options = RenderedQueryOptions(layerIds: mapStyleManager.poiLayerIds, filter: nil) + let rectSize = poiClickableAreaSize + let rect = CGRect(x: point.x - rectSize / 2, y: point.y - rectSize / 2, width: rectSize, height: rectSize) + + let features = try? await mapView.mapboxMap.queryRenderedFeatures(with: rect, options: options) + if let feature = features?.first?.queriedFeature.feature, + case .string(let poiName) = feature[property: .poiName, languageCode: nil], + case .point(let point) = feature.geometry + { + return MapPoint(name: poiName, coordinate: point.coordinates) + } else { + let coordinate = mapView.mapboxMap.coordinate(for: point) + return MapPoint(name: nil, coordinate: coordinate) + } + } +} + +// MARK: - GestureManagerDelegate + +extension NavigationMapView: GestureManagerDelegate { + public nonisolated func gestureManager( + _ gestureManager: MapboxMaps.GestureManager, + didBegin gestureType: MapboxMaps.GestureType + ) { + guard gestureType != .singleTap else { return } + + MainActor.assumingIsolated { + delegate?.navigationMapViewUserDidStartInteraction(self) + } + } + + public nonisolated func gestureManager( + _ gestureManager: MapboxMaps.GestureManager, + didEnd gestureType: MapboxMaps.GestureType, + willAnimate: Bool + ) { + guard gestureType != .singleTap else { return } + + MainActor.assumingIsolated { + delegate?.navigationMapViewUserDidEndInteraction(self) + } + } + + public nonisolated func gestureManager( + _ gestureManager: MapboxMaps.GestureManager, + didEndAnimatingFor gestureType: MapboxMaps.GestureType + ) {} +} + +// MARK: - UIGestureRecognizerDelegate + +extension NavigationMapView: UIGestureRecognizerDelegate { + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + if gestureRecognizer is UITapGestureRecognizer, + otherGestureRecognizer is UITapGestureRecognizer + { + return true + } + + return false + } + + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + if gestureRecognizer is UITapGestureRecognizer, + otherGestureRecognizer == mapView.gestures.doubleTapToZoomInGestureRecognizer + { + return true + } + return false + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+VanishingRouteLine.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+VanishingRouteLine.swift new file mode 100644 index 000000000..dc174a80a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+VanishingRouteLine.swift @@ -0,0 +1,212 @@ +import _MapboxNavigationHelpers +import CoreLocation +import MapboxDirections +import MapboxMaps +import UIKit + +extension NavigationMapView { + struct RoutePoints { + var nestedList: [[[CLLocationCoordinate2D]]] + var flatList: [CLLocationCoordinate2D] + } + + struct RouteLineGranularDistances { + var distance: Double + var distanceArray: [RouteLineDistancesIndex] + } + + struct RouteLineDistancesIndex { + var point: CLLocationCoordinate2D + var distanceRemaining: Double + } + + // MARK: Customizing and Displaying the Route Line(s) + + func initPrimaryRoutePoints(route: Route) { + routePoints = parseRoutePoints(route: route) + routeLineGranularDistances = calculateGranularDistances(routePoints?.flatList ?? []) + } + + /// Transform the route data into nested arrays of legs -> steps -> coordinates. + /// The first and last point of adjacent steps overlap and are duplicated. + func parseRoutePoints(route: Route) -> RoutePoints { + let nestedList = route.legs.map { (routeLeg: RouteLeg) -> [[CLLocationCoordinate2D]] in + return routeLeg.steps.map { (routeStep: RouteStep) -> [CLLocationCoordinate2D] in + if let routeShape = routeStep.shape { + return routeShape.coordinates + } else { + return [] + } + } + } + let flatList = nestedList.flatMap { $0.flatMap { $0.compactMap { $0 } } } + return RoutePoints(nestedList: nestedList, flatList: flatList) + } + + func updateRouteLine(routeProgress: RouteProgress) { + updateIntersectionAnnotations(routeProgress: routeProgress) + if let routes { + mapStyleManager.updateRouteAlertsAnnotations( + navigationRoutes: routes, + excludedRouteAlertTypes: excludedRouteAlertTypes, + distanceTraveled: routeProgress.distanceTraveled + ) + } + + if routeLineTracksTraversal, routes != nil { + guard !routeProgress.routeIsComplete else { + mapStyleManager.removeRoutes() + mapStyleManager.removeArrows() + return + } + + updateUpcomingRoutePointIndex(routeProgress: routeProgress) + } + updateArrow(routeProgress: routeProgress) + } + + func updateAlternatives(routeProgress: RouteProgress?) { + guard let routes = routeProgress?.navigationRoutes ?? routes else { return } + show(routes, routeAnnotationKinds: routeAnnotationKinds) + } + + func updateIntersectionAnnotations(routeProgress: RouteProgress?) { + if let routeProgress, showsIntersectionAnnotations { + mapStyleManager.updateIntersectionAnnotations(routeProgress: routeProgress) + } else { + mapStyleManager.removeIntersectionAnnotations() + } + } + + /// Find and cache the index of the upcoming [RouteLineDistancesIndex]. + func updateUpcomingRoutePointIndex(routeProgress: RouteProgress) { + guard let completeRoutePoints = routePoints, + completeRoutePoints.nestedList.indices.contains(routeProgress.legIndex) + else { + routeRemainingDistancesIndex = nil + return + } + let currentLegProgress = routeProgress.currentLegProgress + let currentStepProgress = routeProgress.currentLegProgress.currentStepProgress + let currentLegSteps = completeRoutePoints.nestedList[routeProgress.legIndex] + var allRemainingPoints = 0 + // Find the count of remaining points in the current step. + let lineString = currentStepProgress.step.shape ?? LineString([]) + // If user hasn't arrived at current step. All the coordinates will be included to the remaining points. + if currentStepProgress.distanceTraveled < 0 { + allRemainingPoints += currentLegSteps[currentLegProgress.stepIndex].count + } else if let startIndex = lineString + .indexedCoordinateFromStart(distance: currentStepProgress.distanceTraveled)?.index, + lineString.coordinates.indices.contains(startIndex) + { + allRemainingPoints += lineString.coordinates.suffix(from: startIndex + 1).dropLast().count + } + + // Add to the count of remaining points all of the remaining points on the current leg, after the current step. + if currentLegProgress.stepIndex < currentLegSteps.endIndex { + var count = 0 + for stepIndex in (currentLegProgress.stepIndex + 1).. RouteLineGranularDistances? { + if coordinates.isEmpty { return nil } + var distance = 0.0 + var indexArray = [RouteLineDistancesIndex?](repeating: nil, count: coordinates.count) + for index in stride(from: coordinates.count - 1, to: 0, by: -1) { + let curr = coordinates[index] + let prev = coordinates[index - 1] + distance += curr.projectedDistance(to: prev) + indexArray[index - 1] = RouteLineDistancesIndex(point: prev, distanceRemaining: distance) + } + indexArray[coordinates.count - 1] = RouteLineDistancesIndex( + point: coordinates[coordinates.count - 1], + distanceRemaining: 0.0 + ) + return RouteLineGranularDistances(distance: distance, distanceArray: indexArray.compactMap { $0 }) + } + + func findClosestCoordinateOnCurrentLine( + coordinate: CLLocationCoordinate2D, + granularDistances: RouteLineGranularDistances, + upcomingIndex: Int + ) -> CLLocationCoordinate2D { + guard granularDistances.distanceArray.indices.contains(upcomingIndex) else { return coordinate } + + var coordinates = [CLLocationCoordinate2D]() + + // Takes the passed 10 points and the upcoming point of route to form a sliced polyline for distance + // calculation, incase of the curved shape of route. + for index in max(0, upcomingIndex - 10)...upcomingIndex { + let point = granularDistances.distanceArray[index].point + coordinates.append(point) + } + + let polyline = LineString(coordinates) + + return polyline.closestCoordinate(to: coordinate)?.coordinate ?? coordinate + } + + /// Updates the fractionTraveled along the route line from the origin point to the indicated point. + /// + /// - parameter coordinate: Current position of the user location. + func calculateFractionTraveled(coordinate: CLLocationCoordinate2D) -> Double? { + guard let granularDistances = routeLineGranularDistances, + let index = routeRemainingDistancesIndex, + granularDistances.distanceArray.indices.contains(index) else { return nil } + let traveledIndex = granularDistances.distanceArray[index] + let upcomingPoint = traveledIndex.point + + // Project coordinate onto current line to properly find offset without an issue of back-growing route line. + let coordinate = findClosestCoordinateOnCurrentLine( + coordinate: coordinate, + granularDistances: granularDistances, + upcomingIndex: index + 1 + ) + + // Take the remaining distance from the upcoming point on the route and extends it by the exact position of the + // puck. + let remainingDistance = traveledIndex.distanceRemaining + upcomingPoint.projectedDistance(to: coordinate) + + // Calculate the percentage of the route traveled. + if granularDistances.distance > 0 { + let offset = (1.0 - remainingDistance / granularDistances.distance) + if offset >= 0 { + return offset + } else { + return nil + } + } + return nil + } + + /// Updates the route style layer and its casing style layer to gradually disappear as the user location puck + /// travels along the displayed route. + /// + /// - parameter coordinate: Current position of the user location. + func travelAlongRouteLine(to coordinate: CLLocationCoordinate2D?) { + guard let coordinate, routes != nil else { return } + if let fraction = calculateFractionTraveled(coordinate: coordinate) { + mapStyleManager.setRouteLineOffset(fraction, for: .main) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView.swift new file mode 100644 index 000000000..222c3d793 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView.swift @@ -0,0 +1,736 @@ +import _MapboxNavigationHelpers +import Combine +import MapboxDirections +import MapboxMaps +import UIKit + +/// `NavigationMapView` is a subclass of `UIView`, which draws `MapView` on its surface and provides +/// convenience functions for adding ``NavigationRoutes`` lines to a map. +@MainActor +open class NavigationMapView: UIView { + private enum Constants { + static let initialMapRect = CGRect(x: 0, y: 0, width: 64, height: 64) + static let initialViewportPadding = UIEdgeInsets(top: 20, left: 20, bottom: 40, right: 20) + } + + /// The `MapView` instance added on top of ``NavigationMapView`` renders navigation-related components. + public let mapView: MapView + + /// ``NavigationCamera``, which allows to control camera states. + public let navigationCamera: NavigationCamera + let mapStyleManager: NavigationMapStyleManager + + /// The object that acts as the navigation delegate of the map view. + public weak var delegate: NavigationMapViewDelegate? + + private var lifetimeSubscriptions: Set = [] + private var viewportDebugView: UIView? + + // Vanishing route line properties + var routePoints: RoutePoints? + var routeLineGranularDistances: RouteLineGranularDistances? + var routeRemainingDistancesIndex: Int? + + private(set) var routes: NavigationRoutes? + + /// The gesture recognizer, that is used to detect taps on waypoints and routes that are currently + /// present on the map. Enabled by default. + public internal(set) var mapViewTapGestureRecognizer: UITapGestureRecognizer! + + /// Initializes ``NavigationMapView`` instance. + /// - Parameters: + /// - location: A publisher that emits current user location. + /// - routeProgress: A publisher that emits route navigation progress. + /// - navigationCameraType: The type of ``NavigationCamera``. Defaults to ``NavigationCameraType/mobile``. + /// which is used for the current instance of ``NavigationMapView``. + /// - heading: A publisher that emits current user heading. Defaults to `nil.` + /// - predictiveCacheManager: An instance of ``PredictiveCacheManager`` used to continuously cache upcoming map + /// tiles. + public init( + location: AnyPublisher, + routeProgress: AnyPublisher, + navigationCameraType: NavigationCameraType = .mobile, + heading: AnyPublisher? = nil, + predictiveCacheManager: PredictiveCacheManager? = nil + ) { + self.mapView = MapView(frame: Constants.initialMapRect).autoresizing() + mapView.location.override( + locationProvider: location.map { [Location(clLocation: $0)] }.eraseToSignal(), + headingProvider: heading?.map { Heading(from: $0) }.eraseToSignal() + ) + + self.mapStyleManager = .init(mapView: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) + self.navigationCamera = NavigationCamera( + mapView, + location: location, + routeProgress: routeProgress, + heading: heading, + navigationCameraType: navigationCameraType + ) + super.init(frame: Constants.initialMapRect) + + mapStyleManager.customizedLayerProvider = customizedLayerProvider + setupMapView() + observeCamera() + enablePredictiveCaching(with: predictiveCacheManager) + subscribeToNavigatonUpdates(routeProgress) + } + + private var currentRouteProgress: RouteProgress? + + // MARK: - Initialization + + private func subscribeToNavigatonUpdates( + _ routeProgressPublisher: AnyPublisher + ) { + routeProgressPublisher + .sink { [weak self] routeProgress in + switch routeProgress { + case nil: + self?.currentRouteProgress = routeProgress + self?.removeRoutes() + case let routeProgress?: + guard let self else { + return + } + let alternativesUpdated = routeProgress.navigationRoutes.alternativeRoutes.map(\.routeId) != routes? + .alternativeRoutes.map(\.routeId) + if routes == nil || routeProgress.routeId != routes?.mainRoute.routeId + || alternativesUpdated + { + show( + routeProgress.navigationRoutes, + routeAnnotationKinds: showsRelativeDurationsOnAlternativeManuever ? + [.relativeDurationsOnAlternativeManuever] : [] + ) + delegate?.navigationMapView( + self, + didAddRedrawActiveGuidanceRoutes: routeProgress.navigationRoutes + ) + } + + currentRouteProgress = routeProgress + updateRouteLine(routeProgress: routeProgress) + } + } + .store(in: &lifetimeSubscriptions) + + routeProgressPublisher + .compactMap { $0 } + .removeDuplicates { $0.legIndex == $1.legIndex } + .sink { [weak self] _ in + self?.updateWaypointsVisiblity() + }.store(in: &lifetimeSubscriptions) + } + + /// `PointAnnotationManager`, which is used to manage addition and removal of a final destination annotation. + /// `PointAnnotationManager` will become valid only after fully loading `MapView` style. + @available( + *, + deprecated, + message: "This property is deprecated and should no longer be used, as the final destination annotation is no longer added to the map. Use 'AnnotationOrchestrator.makePointAnnotationManager()' to create your own annotation manager instead. For more information see the following guide: https://docs.mapbox.com/ios/maps/guides/markers-and-annotations/annotations/#markers" + ) + public private(set) var pointAnnotationManager: PointAnnotationManager? + + private func setupMapView() { + addSubview(mapView) + mapView.pinEdgesToSuperview() + mapView.gestures.delegate = self + mapView.ornaments.options.scaleBar.visibility = .hidden + mapView.preferredFramesPerSecond = 60 + + mapView.location.onPuckRender.sink { [unowned self] data in + travelAlongRouteLine(to: data.location.coordinate) + }.store(in: &lifetimeSubscriptions) + setupGestureRecognizers() + setupUserLocation() + } + + private func observeCamera() { + navigationCamera.cameraStates + .sink { [weak self] cameraState in + guard let self else { return } + delegate?.navigationMapView(self, didChangeCameraState: cameraState) + }.store(in: &lifetimeSubscriptions) + } + + @available(*, unavailable) + override public init(frame: CGRect) { + fatalError("NavigationMapView.init(frame:) is unavailable") + } + + @available(*, unavailable) + public init() { + fatalError("NavigationMapView.init() is unavailable") + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("NavigationMapView.init(coder:) is unavailable") + } + + override open func safeAreaInsetsDidChange() { + super.safeAreaInsetsDidChange() + updateCameraPadding() + } + + // MARK: - Public configuration + + /// The padding applied to the viewport in addition to the safe area. + public var viewportPadding: UIEdgeInsets = Constants.initialViewportPadding { + didSet { updateCameraPadding() } + } + + @_spi(MapboxInternal) public var showsViewportDebugView: Bool = false { + didSet { updateDebugViewportVisibility() } + } + + /// Controls whether to show annotations on intersections, e.g. traffic signals, railroad crossings, yield and stop + /// signs. Defaults to `true`. + public var showsIntersectionAnnotations: Bool = true { + didSet { + updateIntersectionAnnotations(routeProgress: currentRouteProgress) + } + } + + /// Toggles displaying alternative routes. If enabled, view will draw actual alternative route lines on the map. + /// Defaults to `true`. + public var showsAlternatives: Bool = true { + didSet { + updateAlternatives(routeProgress: currentRouteProgress) + } + } + + /// Toggles displaying relative ETA callouts on alternative routes, during active guidance. + /// Defaults to `true`. + public var showsRelativeDurationsOnAlternativeManuever: Bool = true { + didSet { + if showsRelativeDurationsOnAlternativeManuever { + routeAnnotationKinds = [.relativeDurationsOnAlternativeManuever] + } else { + routeAnnotationKinds.removeAll() + } + updateAlternatives(routeProgress: currentRouteProgress) + } + } + + /// Controls whether the main route style layer and its casing disappears as the user location puck travels over it. + /// Defaults to `true`. + /// + /// If `true`, the part of the route that has been traversed will be rendered with full transparency, to give the + /// illusion of a disappearing route. If `false`, the whole route will be shown without traversed part disappearing + /// effect. + public var routeLineTracksTraversal: Bool = true + + /// The maximum distance (in screen points) the user can tap for a selection to be valid when selecting a POI. + public var poiClickableAreaSize: CGFloat = 40 + + /// Controls whether to show restricted portions of a route line. Defaults to true. + public var showsRestrictedAreasOnRoute: Bool = true + + /// Decreases route line opacity based on occlusion from 3D objects. + /// Value `0` disables occlusion, value `1` means fully occluded. Defaults to `0.85`. + public var routeLineOcclusionFactor: Double = 0.85 + + /// Configuration for displaying congestion levels on the route line. + /// Allows to customize the congestion colors and ranges that represent different congestion levels. + public var congestionConfiguration: CongestionConfiguration = .default + + /// Controls whether the traffic should be drawn on the route line or not. Defaults to true. + public var showsTrafficOnRouteLine: Bool = true + + /// Maximum distance (in screen points) the user can tap for a selection to be valid when selecting an alternate + /// route. + public var tapGestureDistanceThreshold: CGFloat = 50 + + /// Controls whether the voice instructions should be drawn on the route line or not. Defaults to `false`. + public var showsVoiceInstructionsOnMap: Bool = false { + didSet { + updateVoiceInstructionsVisiblity() + } + } + + /// Controls whether intermediate waypoints displayed on the route line. Defaults to `true`. + public var showsIntermediateWaypoints: Bool = true { + didSet { + updateWaypointsVisiblity() + } + } + + /// Specifies how the map displays the user’s current location, including the appearance and underlying + /// implementation. + /// + /// By default, this property is set to `PuckType.puck3D(.navigationDefault)` , the bearing source is location + /// course. + public var puckType: PuckType? = .puck3D(.navigationDefault) { + didSet { setupUserLocation() } + } + + /// Specifies if a `Puck` should use `Heading` or `Course` for the bearing. Defaults to `PuckBearing.course`. + public var puckBearing: PuckBearing = .course { + didSet { setupUserLocation() } + } + + /// A custom route line layer position for legacy map styles without slot support. + public var customRouteLineLayerPosition: MapboxMaps.LayerPosition? = nil { + didSet { + mapStyleManager.customRouteLineLayerPosition = customRouteLineLayerPosition + guard let routes else { return } + show(routes, routeAnnotationKinds: routeAnnotationKinds) + } + } + + // MARK: RouteLine Customization + + /// Configures the route line color for the main route. + /// If set, overrides the `.unknown` and `.low` traffic colors. + @objc public dynamic var routeColor: UIColor { + get { + congestionConfiguration.colors.mainRouteColors.unknown + } + set { + congestionConfiguration.colors.mainRouteColors.unknown = newValue + congestionConfiguration.colors.mainRouteColors.low = newValue + } + } + + /// Configures the route line color for alternative routes. + /// If set, overrides the `.unknown` and `.low` traffic colors. + @objc public dynamic var routeAlternateColor: UIColor { + get { + congestionConfiguration.colors.alternativeRouteColors.unknown + } + set { + congestionConfiguration.colors.alternativeRouteColors.unknown = newValue + congestionConfiguration.colors.alternativeRouteColors.low = newValue + } + } + + /// Configures the casing route line color for the main route. + @objc public dynamic var routeCasingColor: UIColor = .defaultRouteCasing + /// Configures the casing route line color for alternative routes. + @objc public dynamic var routeAlternateCasingColor: UIColor = .defaultAlternateLineCasing + /// Configures the color for restricted areas on the route line. + @objc public dynamic var routeRestrictedAreaColor: UIColor = .defaultRouteRestrictedAreaColor + /// Configures the color for the traversed part of the main route. The traversed part is rendered only if the color + /// is not `nil`. + /// Defaults to `nil`. + @objc public dynamic var traversedRouteColor: UIColor? = nil + /// Configures the color of the maneuver arrow. + @objc public dynamic var maneuverArrowColor: UIColor = .defaultManeuverArrow + /// Configures the stroke color of the maneuver arrow. + @objc public dynamic var maneuverArrowStrokeColor: UIColor = .defaultManeuverArrowStroke + + // MARK: Route Annotations Customization + + /// Configures the color of the route annotation for the main route. + @objc public dynamic var routeAnnotationSelectedColor: UIColor = + .defaultSelectedRouteAnnotationColor + /// Configures the color of the route annotation for alternative routes. + @objc public dynamic var routeAnnotationColor: UIColor = .defaultRouteAnnotationColor + /// Configures the text color of the route annotation for the main route. + @objc public dynamic var routeAnnotationSelectedTextColor: UIColor = .defaultSelectedRouteAnnotationTextColor + /// Configures the text color of the route annotation for alternative routes. + @objc public dynamic var routeAnnotationTextColor: UIColor = .defaultRouteAnnotationTextColor + /// Configures the text color of the route annotation for alternative routes when relative duration is greater then + /// the main route. + @objc public dynamic var routeAnnotationMoreTimeTextColor: UIColor = .defaultRouteAnnotationMoreTimeTextColor + /// Configures the text color of the route annotation for alternative routes when relative duration is lesser then + /// the main route. + @objc public dynamic var routeAnnotationLessTimeTextColor: UIColor = .defaultRouteAnnotationLessTimeTextColor + /// Configures the text font of the route annotations. + @objc public dynamic var routeAnnotationTextFont: UIFont = .defaultRouteAnnotationTextFont + /// Configures the waypoint color. + @objc public dynamic var waypointColor: UIColor = .defaultWaypointColor + /// Configures the waypoint stroke color. + @objc public dynamic var waypointStrokeColor: UIColor = .defaultWaypointStrokeColor + + // MARK: - Public methods + + /// Updates the inner navigation camera state. + /// - Parameter navigationCameraState: The navigation camera state. See ``NavigationCameraState`` for the + /// possible values. + public func update(navigationCameraState: NavigationCameraState) { + guard navigationCameraState != navigationCamera.currentCameraState else { return } + navigationCamera.update(cameraState: navigationCameraState) + } + + /// Updates road alerts in the free drive state. In active navigation road alerts are taken automatically from the + /// currently set route. + /// - Parameter roadObjects: An array of road objects to be displayed. + public func updateFreeDriveAlertAnnotations(_ roadObjects: [RoadObjectAhead]) { + mapStyleManager.updateFreeDriveAlertsAnnotations( + roadObjects: roadObjects, + excludedRouteAlertTypes: excludedRouteAlertTypes + ) + } + + // MARK: Customizing and Displaying the Route Line(s) + + /// Visualizes the given routes and it's alternatives, removing any existing from the map. + /// + /// Each route is visualized as a line. Each line is color-coded by traffic congestion, if congestion levels are + /// present. + /// Waypoints along the route are visualized as markers. + /// To only visualize the routes and not the waypoints, or to have more control over the camera, + /// use the ``show(_:routeAnnotationKinds:)`` method. + /// + /// - parameter navigationRoutes: ``NavigationRoutes`` containing routes to visualize. The selected route by + /// `routeIndex` is considered primary, while the remaining routes are displayed as if they are currently deselected + /// or inactive. + /// - parameter routesPresentationStyle: Route lines presentation style. By default the map will be + /// updated to fit all routes. + /// - parameter routeAnnotationKinds: A set of ``RouteAnnotationKind`` that should be displayed. Defaults to + /// ``RouteAnnotationKind/relativeDurationsOnAlternative``. + /// - parameter animated: `true` to asynchronously animate the camera, or `false` to instantaneously + /// zoom and pan the map. Defaults to `false`. + /// - parameter duration: Duration of the animation (in seconds). In case if `animated` parameter + /// is set to `false` this value is ignored. Defaults to `1`. + public func showcase( + _ navigationRoutes: NavigationRoutes, + routesPresentationStyle: RoutesPresentationStyle = .all(), + routeAnnotationKinds: Set = [.relativeDurationsOnAlternative], + animated: Bool = false, + duration: TimeInterval = 1.0 + ) { + show(navigationRoutes, routeAnnotationKinds: routeAnnotationKinds) + mapStyleManager.removeArrows() + + fitCamera( + routes: navigationRoutes, + routesPresentationStyle: routesPresentationStyle, + animated: animated, + duration: duration + ) + } + + private(set) var routeAnnotationKinds: Set = [] + + /// Represents a set of ``RoadAlertType`` values that should be hidden from the map display. + /// By default, this is an empty set, which indicates that all road alerts will be displayed. + /// + /// - Note: If specific `RoadAlertType` values are added to this set, those alerts will be + /// excluded from the map rendering. + public var excludedRouteAlertTypes: RoadAlertType = [] { + didSet { + guard let navigationRoutes = routes else { + return + } + + mapStyleManager.updateRouteAlertsAnnotations( + navigationRoutes: navigationRoutes, + excludedRouteAlertTypes: excludedRouteAlertTypes + ) + } + } + + /// Visualizes the given routes and it's alternatives, removing any existing from the map. + /// + /// Each route is visualized as a line. Each line is color-coded by traffic congestion, if congestion + /// levels are present. To also visualize waypoints and zoom the map to fit, + /// use the ``showcase(_:routesPresentationStyle:routeAnnotationKinds:animated:duration:)`` method. + /// + /// To undo the effects of this method, use ``removeRoutes()`` method. + /// - Parameters: + /// - navigationRoutes: ``NavigationRoutes`` to be displayed on the map. + /// - routeAnnotationKinds: A set of ``RouteAnnotationKind`` that should be displayed. + public func show( + _ navigationRoutes: NavigationRoutes, + routeAnnotationKinds: Set + ) { + removeRoutes() + routes = navigationRoutes + self.routeAnnotationKinds = routeAnnotationKinds + let mainRoute = navigationRoutes.mainRoute.route + if routeLineTracksTraversal { + initPrimaryRoutePoints(route: mainRoute) + } + mapStyleManager.updateRoutes( + navigationRoutes, + config: mapStyleConfig, + featureProvider: customRouteLineFeatureProvider + ) + updateWaypointsVisiblity() + if showsVoiceInstructionsOnMap { + mapStyleManager.updateVoiceInstructions(route: mainRoute) + } + mapStyleManager.updateRouteAnnotations( + navigationRoutes: navigationRoutes, + annotationKinds: routeAnnotationKinds, + config: mapStyleConfig + ) + mapStyleManager.updateRouteAlertsAnnotations( + navigationRoutes: navigationRoutes, + excludedRouteAlertTypes: excludedRouteAlertTypes + ) + } + + /// Removes routes and all visible annotations from the map. + public func removeRoutes() { + routes = nil + routeLineGranularDistances = nil + routeRemainingDistancesIndex = nil + mapStyleManager.removeAllFeatures() + } + + func updateArrow(routeProgress: RouteProgress) { + if routeProgress.currentLegProgress.followOnStep != nil { + mapStyleManager.updateArrows( + route: routeProgress.route, + legIndex: routeProgress.legIndex, + stepIndex: routeProgress.currentLegProgress.stepIndex + 1, + config: mapStyleConfig + ) + } else { + removeArrows() + } + } + + /// Removes the `RouteStep` arrow from the `MapView`. + func removeArrows() { + mapStyleManager.removeArrows() + } + + // MARK: - Debug Viewport + + private func updateDebugViewportVisibility() { + if showsViewportDebugView { + let viewportDebugView = with(UIView(frame: .zero)) { + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.blue.cgColor + $0.backgroundColor = .clear + } + addSubview(viewportDebugView) + self.viewportDebugView = viewportDebugView + viewportDebugView.isUserInteractionEnabled = false + updateViewportDebugView() + } else { + viewportDebugView?.removeFromSuperview() + viewportDebugView = nil + } + } + + private func updateViewportDebugView() { + viewportDebugView?.frame = bounds.inset(by: navigationCamera.viewportPadding) + } + + // MARK: - Camera + + private func updateCameraPadding() { + let padding = viewportPadding + let safeAreaInsets = safeAreaInsets + + navigationCamera.viewportPadding = .init( + top: safeAreaInsets.top + padding.top, + left: safeAreaInsets.left + padding.left, + bottom: safeAreaInsets.bottom + padding.bottom, + right: safeAreaInsets.right + padding.right + ) + updateViewportDebugView() + } + + private func fitCamera( + routes: NavigationRoutes, + routesPresentationStyle: RoutesPresentationStyle, + animated: Bool = false, + duration: TimeInterval + ) { + navigationCamera.stop() + let coordinates: [CLLocationCoordinate2D] + switch routesPresentationStyle { + case .main, .all(shouldFit: false): + coordinates = routes.mainRoute.route.shape?.coordinates ?? [] + case .all(true): + let routes = [routes.mainRoute.route] + routes.alternativeRoutes.map(\.route) + coordinates = MultiLineString(routes.compactMap(\.shape?.coordinates)).coordinates.flatMap { $0 } + } + let initialCameraOptions = CameraOptions( + padding: navigationCamera.viewportPadding, + bearing: 0, + pitch: 0 + ) + do { + let cameraOptions = try mapView.mapboxMap.camera( + for: coordinates, + camera: initialCameraOptions, + coordinatesPadding: nil, + maxZoom: nil, + offset: nil + ) + mapView.camera.ease(to: cameraOptions, duration: animated ? duration : 0.0) + } catch { + Log.error("Failed to fit the camera: \(error.localizedDescription)", category: .navigationUI) + } + } + + // MARK: - Localization + + /// Attempts to localize labels into the preferred language. + /// + /// This method automatically modifies the `SymbolLayer.textField` property of any symbol style + /// layer whose source is the [Mapbox Streets + /// source](https://docs.mapbox.com/vector-tiles/reference/mapbox-streets-v8/#overview). + /// The user can set the system’s preferred language in Settings, General Settings, Language & Region. + /// + /// This method avoids localizing road labels into the preferred language, in an effort + /// to match road signage and the turn banner, which always display road names and exit destinations + /// in the local language. + /// + /// - parameter locale: `Locale` in which the map will attempt to be localized. + /// To use the system’s preferred language, if supported, specify nil. Defaults to `nil`. + public func localizeLabels(locale: Locale? = nil) { + guard let preferredLocale = locale ?? VectorSource + .preferredMapboxStreetsLocale(for: nil) else { return } + mapView.localizeLabels(into: preferredLocale) + } + + private func updateVoiceInstructionsVisiblity() { + if showsVoiceInstructionsOnMap { + mapStyleManager.removeVoiceInstructions() + } else if let routes { + mapStyleManager.updateVoiceInstructions(route: routes.mainRoute.route) + } + } + + private var customRouteLineFeatureProvider: RouteLineFeatureProvider { + .init { [weak self] identifier, sourceIdentifier in + guard let self else { return nil } + return delegate?.navigationMapView( + self, + routeLineLayerWithIdentifier: identifier, + sourceIdentifier: sourceIdentifier + ) + } customRouteCasingLineLayer: { [weak self] identifier, sourceIdentifier in + guard let self else { return nil } + return delegate?.navigationMapView( + self, + routeCasingLineLayerWithIdentifier: identifier, + sourceIdentifier: sourceIdentifier + ) + } customRouteRestrictedAreasLineLayer: { [weak self] identifier, sourceIdentifier in + guard let self else { return nil } + return delegate?.navigationMapView( + self, + routeRestrictedAreasLineLayerWithIdentifier: identifier, + sourceIdentifier: sourceIdentifier + ) + } + } + + private var waypointsFeatureProvider: WaypointFeatureProvider { + .init { [weak self] waypoints, legIndex in + guard let self else { return nil } + return delegate?.navigationMapView(self, shapeFor: waypoints, legIndex: legIndex) + } customCirleLayer: { [weak self] identifier, sourceIdentifier in + guard let self else { return nil } + return delegate?.navigationMapView( + self, + waypointCircleLayerWithIdentifier: identifier, + sourceIdentifier: sourceIdentifier + ) + } customSymbolLayer: { [weak self] identifier, sourceIdentifier in + guard let self else { return nil } + return delegate?.navigationMapView( + self, + waypointSymbolLayerWithIdentifier: identifier, + sourceIdentifier: sourceIdentifier + ) + } + } + + private var customizedLayerProvider: CustomizedLayerProvider { + .init { [weak self] in + guard let self else { return $0 } + return customizedLayer($0) + } + } + + private func customizedLayer(_ layer: T) -> T where T: Layer { + guard let customizedLayer = delegate?.navigationMapView(self, willAdd: layer) else { + return layer + } + guard let customizedLayer = customizedLayer as? T else { + preconditionFailure("The customized layer should have the same layer type as the default layer.") + } + return customizedLayer + } + + private func updateWaypointsVisiblity() { + guard let mainRoute = routes?.mainRoute.route else { + mapStyleManager.removeWaypoints() + return + } + + mapStyleManager.updateWaypoints( + route: mainRoute, + legIndex: currentRouteProgress?.legIndex ?? 0, + config: mapStyleConfig, + featureProvider: waypointsFeatureProvider + ) + } + + // - MARK: User Tracking Features + + private func setupUserLocation() { + mapView.location.options.puckType = puckType ?? .puck2D(.emptyPuck) + mapView.location.options.puckBearing = puckBearing + mapView.location.options.puckBearingEnabled = true + } + + // MARK: Configuring Cache and Tiles Storage + + private var predictiveCacheMapObserver: MapboxMaps.Cancelable? = nil + + /// Setups the Predictive Caching mechanism using provided Options. + /// + /// This will handle all the required manipulations to enable the feature and maintain it during the navigations. + /// Once enabled, it will be present as long as `NavigationMapView` is retained. + /// + /// - parameter options: options, controlling caching parameters like area radius and concurrent downloading + /// threads. + private func enablePredictiveCaching(with predictiveCacheManager: PredictiveCacheManager?) { + predictiveCacheMapObserver?.cancel() + + guard let predictiveCacheManager else { + predictiveCacheMapObserver = nil + return + } + + predictiveCacheManager.updateMapControllers(mapView: mapView) + predictiveCacheMapObserver = mapView.mapboxMap.onStyleLoaded.observe { [ + weak self, + predictiveCacheManager + ] _ in + guard let self else { return } + + predictiveCacheManager.updateMapControllers(mapView: mapView) + } + } + + private var mapStyleConfig: MapStyleConfig { + .init( + routeCasingColor: routeCasingColor, + routeAlternateCasingColor: routeAlternateCasingColor, + routeRestrictedAreaColor: routeRestrictedAreaColor, + traversedRouteColor: traversedRouteColor, + maneuverArrowColor: maneuverArrowColor, + maneuverArrowStrokeColor: maneuverArrowStrokeColor, + routeAnnotationSelectedColor: routeAnnotationSelectedColor, + routeAnnotationColor: routeAnnotationColor, + routeAnnotationSelectedTextColor: routeAnnotationSelectedTextColor, + routeAnnotationTextColor: routeAnnotationTextColor, + routeAnnotationMoreTimeTextColor: routeAnnotationMoreTimeTextColor, + routeAnnotationLessTimeTextColor: routeAnnotationLessTimeTextColor, + routeAnnotationTextFont: routeAnnotationTextFont, + routeLineTracksTraversal: routeLineTracksTraversal, + isRestrictedAreaEnabled: showsRestrictedAreasOnRoute, + showsTrafficOnRouteLine: showsTrafficOnRouteLine, + showsAlternatives: showsAlternatives, + showsIntermediateWaypoints: showsIntermediateWaypoints, + occlusionFactor: .constant(routeLineOcclusionFactor), + congestionConfiguration: congestionConfiguration, + waypointColor: waypointColor, + waypointStrokeColor: waypointStrokeColor + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapViewDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapViewDelegate.swift new file mode 100644 index 000000000..e1cbf6c3e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapViewDelegate.swift @@ -0,0 +1,289 @@ +import MapboxDirections +import MapboxMaps +import Turf +import UIKit + +/// The ``NavigationMapViewDelegate`` provides methods for responding to events triggered by the ``NavigationMapView``. +@MainActor +public protocol NavigationMapViewDelegate: AnyObject, UnimplementedLogging { + /// Tells the receiver that the user has selected an alternative route by interacting with the map view. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView``. + /// - alternativeRoute: The selected alternative route. + func navigationMapView(_ navigationMapView: NavigationMapView, didSelect alternativeRoute: AlternativeRoute) + + /// Tells the receiver that the user has tapped on a POI. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView``. + /// - mapPoint: A selected ``MapPoint``. + func navigationMapView(_ navigationMapView: NavigationMapView, userDidTap mapPoint: MapPoint) + + /// Tells the receiver that the user has long tapped on a POI. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView``. + /// - mapPoint: A selected ``MapPoint``. + func navigationMapView(_ navigationMapView: NavigationMapView, userDidLongTap mapPoint: MapPoint) + + /// Tells the receiver that the user has started interacting with the map view, e.g. with panning gesture. + /// - Parameter navigationMapView: The ``NavigationMapView``. + func navigationMapViewUserDidStartInteraction(_ navigationMapView: NavigationMapView) + + /// Tells the receiver that the user has stopped interacting with the map view. + /// - Parameter navigationMapView: The ``NavigationMapView``. + func navigationMapViewUserDidEndInteraction(_ navigationMapView: NavigationMapView) + + /// Tells the receiver that the camera changed its state. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView`` object. + /// - cameraState: A new camera state. + func navigationMapView( + _ navigationMapView: NavigationMapView, + didChangeCameraState cameraState: NavigationCameraState + ) + + /// Tells the receiver that a waypoint was selected. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView``. + /// - waypoint: The waypoint that was selected. + func navigationMapView(_ navigationMapView: NavigationMapView, didSelect waypoint: Waypoint) + + /// Tells the receiver that the final destination `PointAnnotation` was added to the ``NavigationMapView``. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView`` object. + /// - finalDestinationAnnotation: The point annotation that was added to the map view. + /// - pointAnnotationManager: The object that manages the point annotation in the map view. + @available( + *, + deprecated, + message: "This method is deprecated and should no longer be used, as the final destination annotation is no longer added to the map." + ) + func navigationMapView( + _ navigationMapView: NavigationMapView, + didAdd finalDestinationAnnotation: PointAnnotation, + pointAnnotationManager: PointAnnotationManager + ) + + /// Tells the reciever that ``NavigationMapView`` has updated the displayed ``NavigationRoutes`` for the active + /// guidance. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView`` object. + /// - navigationRoutes: New displayed ``NavigationRoutes`` object. + func navigationMapView( + _ navigationMapView: NavigationMapView, + didAddRedrawActiveGuidanceRoutes navigationRoutes: NavigationRoutes + ) + + // MARK: Supplying Waypoint(s) Data + + /// Asks the receiver to return a `CircleLayer` for waypoints, given an identifier and source. + /// This method is invoked any time waypoints are added or shown. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView`` object. + /// - identifier: The `CircleLayer` identifier. + /// - sourceIdentifier: Identifier of the source, which contains the waypoint data that this method would style. + /// - Returns: A `CircleLayer` that the map applies to all waypoints. + func navigationMapView( + _ navigationMapView: NavigationMapView, + waypointCircleLayerWithIdentifier identifier: String, + sourceIdentifier: String + ) -> CircleLayer? + + /// Asks the receiver to return a `SymbolLayer` for intermediate waypoint symbols, given an identifier and source. + /// This method is invoked any time intermediate waypoints are added or shown. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView`` object. + /// - identifier: The `SymbolLayer` identifier. + /// - sourceIdentifier: Identifier of the source, which contains the waypoint data that this method would style. + /// - Returns: A `SymbolLayer` that the map applies to all waypoint symbols. + func navigationMapView( + _ navigationMapView: NavigationMapView, + waypointSymbolLayerWithIdentifier identifier: String, + sourceIdentifier: String + ) -> SymbolLayer? + + /// Asks the receiver to return a `FeatureCollection` that describes the geometry of waypoints. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView`` object. + /// - waypoints: The waypoints to be displayed on the map. + /// - legIndex: The index of the current leg during navigation. + /// - Returns: Optionally, a `FeatureCollection` that defines the shape of the waypoint, or `nil` to use default + /// behavior. + func navigationMapView(_ navigationMapView: NavigationMapView, shapeFor waypoints: [Waypoint], legIndex: Int) + -> FeatureCollection? + + // MARK: Supplying Route Line(s) Data + + /// Asks the receiver to return a `LineLayer` for the route line, given a layer identifier and a source identifier. + /// This method is invoked when the map view loads and any time routes are added. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView`` object. + /// - identifier: The `LineLayer` identifier. + /// - sourceIdentifier: Identifier of the source, which contains the route data that this method would style. + /// - Returns: A `LineLayer` that is applied to the route line. + func navigationMapView( + _ navigationMapView: NavigationMapView, + routeLineLayerWithIdentifier identifier: String, + sourceIdentifier: String + ) -> LineLayer? + + /// Asks the receiver to return a `LineLayer` for the casing layer that surrounds route line, given a layer + /// identifier and a source identifier. This method is invoked when the map view loads and any time routes are + /// added. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView`` object. + /// - identifier: The `LineLayer` identifier. + /// - sourceIdentifier: Identifier of the source, which contains the route data that this method would style. + /// - Returns: A `LineLayer` that is applied as a casing around the route line. + func navigationMapView( + _ navigationMapView: NavigationMapView, + routeCasingLineLayerWithIdentifier identifier: String, + sourceIdentifier: String + ) -> LineLayer? + + /// Asks the receiver to return a `LineLayer` for highlighting restricted areas portions of the route, given a layer + /// identifier and a source identifier. This method is invoked when + /// ``NavigationMapView/showsRestrictedAreasOnRoute`` is enabled, the map view loads and any time routes are added. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView`` object. + /// - identifier: The `LineLayer` identifier. + /// - sourceIdentifier: Identifier of the source, which contains the route data that this method would style. + /// - Returns: A `LineLayer` that is applied as restricted areas on the route line. + func navigationMapView( + _ navigationMapView: NavigationMapView, + routeRestrictedAreasLineLayerWithIdentifier identifier: String, + sourceIdentifier: String + ) -> LineLayer? + + /// Asks the receiver to adjust the default layer which will be added to the map view and return a `Layer`. + /// This method is invoked when the map view loads and any time a layer will be added. + /// - Parameters: + /// - navigationMapView: The ``NavigationMapView`` object. + /// - layer: A default `Layer` generated by the navigationMapView. + /// - Returns: An adjusted `Layer` that will be added to the map view by the SDK. + func navigationMapView(_ navigationMapView: NavigationMapView, willAdd layer: Layer) -> Layer? +} + +extension NavigationMapViewDelegate { + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView( + _ navigationMapView: NavigationMapView, + routeLineLayerWithIdentifier identifier: String, + sourceIdentifier: String + ) -> LineLayer? { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + return nil + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView( + _ navigationMapView: NavigationMapView, + routeCasingLineLayerWithIdentifier identifier: String, + sourceIdentifier: String + ) -> LineLayer? { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + return nil + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView( + _ navigationMapView: NavigationMapView, + routeRestrictedAreasLineLayerWithIdentifier identifier: String, + sourceIdentifier: String + ) -> LineLayer? { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + return nil + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView( + _ navigationMapView: NavigationMapView, + didSelect alternativeRoute: AlternativeRoute + ) { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView(_ navigationMapView: NavigationMapView, userDidTap mapPoint: MapPoint) { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView(_ navigationMapView: NavigationMapView, userDidLongTap mapPoint: MapPoint) { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapViewUserDidStartInteraction(_ navigationMapView: NavigationMapView) { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapViewUserDidEndInteraction(_ navigationMapView: NavigationMapView) { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView( + _ navigationMapView: NavigationMapView, + didChangeCameraState cameraState: NavigationCameraState + ) { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + } + + public func navigationMapView(_ navigationMapView: NavigationMapView, didSelect waypoint: Waypoint) { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView( + _ navigationMapView: NavigationMapView, + didAdd finalDestinationAnnotation: PointAnnotation, + pointAnnotationManager: PointAnnotationManager + ) { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView( + _ navigationMapView: NavigationMapView, + didAddRedrawActiveGuidanceRoutes navigationRoutes: NavigationRoutes + ) { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView( + _ navigationMapView: NavigationMapView, + shapeFor waypoints: [Waypoint], + legIndex: Int + ) -> FeatureCollection? { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + return nil + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView( + _ navigationMapView: NavigationMapView, + waypointCircleLayerWithIdentifier identifier: String, + sourceIdentifier: String + ) -> CircleLayer? { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + return nil + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView( + _ navigationMapView: NavigationMapView, + waypointSymbolLayerWithIdentifier identifier: String, + sourceIdentifier: String + ) -> SymbolLayer? { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + return nil + } + + /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. + public func navigationMapView(_ navigationMapView: NavigationMapView, willAdd layer: Layer) -> Layer? { + logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) + return nil + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Array.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Array.swift new file mode 100644 index 000000000..633462993 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Array.swift @@ -0,0 +1,159 @@ +import CoreGraphics +import CoreLocation +import Foundation +import MapboxDirections +import Turf + +extension Array { + /// Conditionally remove each element depending on the elements immediately preceding and following it. + /// + /// - parameter shouldBeRemoved: A closure that is called once for each element in reverse order from last to first. + /// The closure accepts the + /// following arguments: the preceding element in the (unreversed) array, the element itself, and the following + /// element in the (unreversed) array. + mutating func removeSeparators(where shouldBeRemoved: (Element?, Element, Element?) throws -> Bool) rethrows { + for (index, element) in enumerated().reversed() { + let precedingElement = lazy.prefix(upTo: index).last + let followingElement = lazy.suffix(from: self.index(after: index)).first + if try shouldBeRemoved(precedingElement, element, followingElement) { + remove(at: index) + } + } + } +} + +extension [RouteStep] { + // Find the longest contiguous series of RouteSteps connected to the first one. + // + // tolerance: -- Maximum distance between the end of one RouteStep and the start of the next to still consider them connected. Defaults to 100 meters + func continuousShape(tolerance: CLLocationDistance = 100) -> LineString? { + guard count > 0 else { return nil } + guard count > 1 else { return self[0].shape } + + let stepShapes = compactMap(\.shape) + let filteredStepShapes = zip(stepShapes, stepShapes.suffix(from: 1)).filter { + guard let maneuverLocation = $1.coordinates.first else { return false } + + return $0.coordinates.last?.distance(to: maneuverLocation) ?? Double.greatestFiniteMagnitude < tolerance + } + + let coordinates = filteredStepShapes.flatMap { firstLine, _ -> [CLLocationCoordinate2D] in + return firstLine.coordinates + } + + return LineString(coordinates) + } +} + +extension Array where Element: NSAttributedString { + /// Returns a new attributed string by concatenating the elements of the array, adding the given separator between + /// each element. + func joined(separator: NSAttributedString = .init()) -> NSAttributedString { + guard let first else { + return NSAttributedString() + } + + let joinedAttributedString = NSMutableAttributedString(attributedString: first) + for element in dropFirst() { + joinedAttributedString.append(separator) + joinedAttributedString.append(element) + } + return joinedAttributedString + } +} + +extension Array where Iterator.Element == CLLocationCoordinate2D { + /// Returns an array of congestion segments by associating the given congestion levels with the coordinates of + /// the respective line segments that they apply to. + /// + /// This method coalesces consecutive line segments that have the same congestion level. + /// + /// For each item in the `CongestionSegment` collection a `CongestionLevel` substitution will take place that + /// has a streets road class contained in the `roadClassesWithOverriddenCongestionLevels` collection. + /// For each of these items the `CongestionLevel` for `.unknown` traffic congestion will be replaced with the + /// `.low` traffic congestion. + /// + /// - parameter congestionLevels: The congestion levels along a leg. There should be one fewer congestion levels + /// than coordinates. + /// - parameter streetsRoadClasses: A collection of streets road classes for each geometry index in + /// `Intersection`. There should be the same amount of `streetsRoadClasses` and `congestions`. + /// - parameter roadClassesWithOverriddenCongestionLevels: Streets road classes for which a `CongestionLevel` + /// substitution should occur. + /// - returns: A list of `CongestionSegment` tuples with coordinate and congestion level. + func combined( + _ congestionLevels: [CongestionLevel], + streetsRoadClasses: [MapboxStreetsRoadClass?]? = nil, + roadClassesWithOverriddenCongestionLevels: Set? = nil + ) + -> [CongestionSegment] { + var segments: [CongestionSegment] = [] + segments.reserveCapacity(congestionLevels.count) + + var index = 0 + for (firstSegment, congestionLevel) in zip(zip(self, suffix(from: 1)), congestionLevels) { + let coordinates = [firstSegment.0, firstSegment.1] + + var overriddenCongestionLevel = congestionLevel + if let streetsRoadClasses, + let roadClassesWithOverriddenCongestionLevels, + streetsRoadClasses.indices.contains(index), + let streetsRoadClass = streetsRoadClasses[index], + congestionLevel == .unknown, + roadClassesWithOverriddenCongestionLevels.contains(streetsRoadClass) + { + overriddenCongestionLevel = .low + } + + if segments.last?.1 == overriddenCongestionLevel { + segments[segments.count - 1].0 += [firstSegment.1] + } else { + segments.append((coordinates, overriddenCongestionLevel)) + } + + index += 1 + } + + return segments + } + + /// Returns an array of road segments by associating road classes of corresponding line segments. + /// + /// Adjacent segments with the same `combiningRoadClasses` will be merged together. + /// + /// - parameter roadClasses: An array of `RoadClasses`along given segment. There should be one fewer congestion + /// levels than coordinates. + /// - parameter combiningRoadClasses: `RoadClasses` which will be joined if they are neighbouring each other. + /// - returns: A list of `RoadClassesSegment` tuples with coordinate and road class. + func combined( + _ roadClasses: [RoadClasses?], + combiningRoadClasses: RoadClasses? = nil + ) -> [RoadClassesSegment] { + var segments: [RoadClassesSegment] = [] + segments.reserveCapacity(roadClasses.count) + + var index = 0 + for (firstSegment, currentRoadClass) in zip(zip(self, suffix(from: 1)), roadClasses) { + let coordinates = [firstSegment.0, firstSegment.1] + var definedRoadClass = currentRoadClass ?? RoadClasses() + definedRoadClass = combiningRoadClasses?.intersection(definedRoadClass) ?? definedRoadClass + + if segments.last?.1 == definedRoadClass { + segments[segments.count - 1].0 += [firstSegment.1] + } else { + segments.append((coordinates, definedRoadClass)) + } + + index += 1 + } + + return segments + } +} + +extension Array { + func chunked(into size: Int) -> [[Element]] { + return stride(from: 0, to: count, by: size).map { + Array(self[$0.. Double { + let distanceArray: [Double] = [ + projectX(longitude) - projectX(coordinate.longitude), + projectY(latitude) - projectY(coordinate.latitude), + ] + return (distanceArray[0] * distanceArray[0] + distanceArray[1] * distanceArray[1]).squareRoot() + } + + private func projectX(_ x: Double) -> Double { + return x / 360.0 + 0.5 + } + + private func projectY(_ y: Double) -> Double { + let sinValue = sin(y * Double.pi / 180) + let newYValue = 0.5 - 0.25 * log((1 + sinValue) / (1 - sinValue)) / Double.pi + if newYValue < 0 { + return 0.0 + } else if newYValue > 1 { + return 1.1 + } else { + return newYValue + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/CongestionSegment.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/CongestionSegment.swift new file mode 100644 index 000000000..2e92fc485 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/CongestionSegment.swift @@ -0,0 +1,6 @@ +import CoreLocation +import MapboxDirections + +/// A tuple that pairs an array of coordinates with the level of +/// traffic congestion along these coordinates. +typealias CongestionSegment = ([CLLocationCoordinate2D], CongestionLevel) diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Cosntants.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Cosntants.swift new file mode 100644 index 000000000..79c291bb0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Cosntants.swift @@ -0,0 +1,22 @@ +import CoreLocation + +/// A stop dictionary representing the default line widths of the route line by zoom level. +public let RouteLineWidthByZoomLevel: [Double: Double] = [ + 10.0: 8.0, + 13.0: 9.0, + 16.0: 11.0, + 19.0: 22.0, + 22.0: 28.0, +] + +/// Attribute name for the route line that is used for identifying restricted areas along the route. +let RestrictedRoadClassAttribute = "isRestrictedRoad" + +/// Attribute name for the route line that is used for identifying whether a RouteLeg is the current active leg. +let CurrentLegAttribute = "isCurrentLeg" + +/// Attribute name for the route line that is used for identifying different `CongestionLevel` along the route. +let CongestionAttribute = "congestion" + +/// The distance of fading color change between two different congestion level segments in meters. +let GradientCongestionFadingDistance: CLLocationDistance = 30.0 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Expression++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Expression++.swift new file mode 100644 index 000000000..73052a7d8 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Expression++.swift @@ -0,0 +1,61 @@ +import MapboxMaps +import UIKit + +extension MapboxMaps.Expression { + static func routeLineWidthExpression(_ multiplier: Double = 1.0) -> MapboxMaps.Expression { + return Exp(.interpolate) { + Exp(.linear) + Exp(.zoom) + RouteLineWidthByZoomLevel.multiplied(by: multiplier) + } + } + + static func routeCasingLineWidthExpression(_ multiplier: Double = 1.0) -> MapboxMaps.Expression { + routeLineWidthExpression(multiplier * 1.5) + } + + static func routeLineGradientExpression( + _ gradientStops: [Double: UIColor], + lineBaseColor: UIColor, + isSoft: Bool = false + ) -> MapboxMaps.Expression { + if isSoft { + return Exp(.interpolate) { + Exp(.linear) + Exp(.lineProgress) + gradientStops + } + } else { + return Exp(.step) { + Exp(.lineProgress) + lineBaseColor + gradientStops + } + } + } + + static func buildingExtrusionHeightExpression(_ hightProperty: String) -> MapboxMaps.Expression { + return Exp(.interpolate) { + Exp(.linear) + Exp(.zoom) + 13 + 0 + 13.25 + Exp(.get) { + hightProperty + } + } + } +} + +extension [Double: Double] { + /// Returns a copy of the stop dictionary with each value multiplied by the given factor. + public func multiplied(by factor: Double) -> Dictionary { + var newCameraStop: [Double: Double] = [:] + for stop in self { + let newValue = stop.value * factor + newCameraStop[stop.key] = newValue + } + return newCameraStop + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Feature++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Feature++.swift new file mode 100644 index 000000000..1ee59c5e6 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Feature++.swift @@ -0,0 +1,28 @@ +import Foundation +import Turf + +extension Feature { + var featureIdentifier: Int64? { + guard let featureIdentifier = identifier else { return nil } + + switch featureIdentifier { + case .string(let identifier): + return Int64(identifier) + case .number(let identifier): + return Int64(identifier) + } + } + + enum Property: String { + case poiName = "name" + } + + subscript(property key: Property, languageCode keySuffix: String?) -> JSONValue? { + let jsonValue: JSONValue? = if let keySuffix, let value = properties?["\(key.rawValue)_\(keySuffix)"] { + value + } else { + properties?[key.rawValue].flatMap { $0 } + } + return jsonValue + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/MapboxMap+Async.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/MapboxMap+Async.swift new file mode 100644 index 000000000..ffcf45e4c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/MapboxMap+Async.swift @@ -0,0 +1,54 @@ +import _MapboxNavigationHelpers +import MapboxMaps + +extension MapboxMap { + @MainActor + func queryRenderedFeatures( + with point: CGPoint, + options: RenderedQueryOptions? = nil + ) async throws -> [QueriedRenderedFeature] { + let state: CancellableAsyncState = .init() + + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + let cancellable = queryRenderedFeatures(with: point, options: options) { result in + continuation.resume(with: result) + } + state.activate(with: .init(cancellable)) + } + } onCancel: { + state.cancel() + } + } + + @MainActor + func queryRenderedFeatures( + with rect: CGRect, + options: RenderedQueryOptions? = nil + ) async throws -> [QueriedRenderedFeature] { + let state: CancellableAsyncState = .init() + + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + let cancellable = queryRenderedFeatures(with: rect, options: options) { result in + continuation.resume(with: result) + } + state.activate(with: .init(cancellable)) + } + } onCancel: { + state.cancel() + } + } +} + +private final class AnyMapboxMapsCancelable: CancellableAsyncStateValue { + private let mapboxMapsCancellable: any MapboxMaps.Cancelable + + init(_ mapboxMapsCancellable: any MapboxMaps.Cancelable) { + self.mapboxMapsCancellable = mapboxMapsCancellable + } + + func cancel() { + mapboxMapsCancellable.cancel() + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift new file mode 100644 index 000000000..1602e79fa --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift @@ -0,0 +1,37 @@ +import Foundation + +extension NavigationMapView { + static let identifier = "com.mapbox.navigation.core" + + @MainActor + enum LayerIdentifier { + static let puck2DLayer: String = "puck" + static let puck3DLayer: String = "puck-model-layer" + static let poiLabelLayer: String = "poi-label" + static let transitLabelLayer: String = "transit-label" + static let airportLabelLayer: String = "airport-label" + + static var clickablePoiLabels: [String] { + [ + LayerIdentifier.poiLabelLayer, + LayerIdentifier.transitLabelLayer, + LayerIdentifier.airportLabelLayer, + ] + } + } + + enum ImageIdentifier { + static let markerImage = "default_marker" + static let midpointMarkerImage = "midpoint_marker" + static let trafficSignal = "traffic_signal" + static let railroadCrossing = "railroad_crossing" + static let yieldSign = "yield_sign" + static let stopSign = "stop_sign" + static let searchAnnotationImage = "search_annotation" + static let selectedSearchAnnotationImage = "search_annotation_selected" + } + + enum ModelKeyIdentifier { + static let modelSouce = "puck-model" + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/PuckConfigurations.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/PuckConfigurations.swift new file mode 100644 index 000000000..08c4d1301 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/PuckConfigurations.swift @@ -0,0 +1,41 @@ +import _MapboxNavigationHelpers +@_spi(Experimental) import MapboxMaps +import UIKit + +extension Puck3DConfiguration { + private static let modelURL = Bundle.mapboxNavigationUXCore.url(forResource: "3DPuck", withExtension: "glb")! + + /// Default 3D user puck configuration + public static let navigationDefault = Puck3DConfiguration( + model: Model(uri: modelURL), + modelScale: .constant([1.5, 1.5, 1.5]), + modelOpacity: .constant(1), + // Turn off shadows as it greatly affect performance due to constant shadow recalculation. + modelCastShadows: .constant(false), + modelReceiveShadows: .constant(false), + modelEmissiveStrength: .constant(0) + ) +} + +extension Puck2DConfiguration { + public static let navigationDefault = Puck2DConfiguration( + topImage: UIColor.clear.image(CGSize(width: 1.0, height: 1.0)), + bearingImage: .init(named: "puck", in: .mapboxNavigationUXCore, compatibleWith: nil), + showsAccuracyRing: false, + opacity: 1 + ) + + static let emptyPuck: Self = { + // Since Mapbox Maps will not provide location data in case if `LocationOptions.puckType` is + // set to nil, we have to draw empty and transparent `UIImage` instead of puck. This is used + // in case when user wants to stop showing location puck or draw a custom one. + let clearImage = UIColor.clear.image(CGSize(width: 1.0, height: 1.0)) + return Puck2DConfiguration( + topImage: clearImage, + bearingImage: clearImage, + shadowImage: clearImage, + scale: nil, + showsAccuracyRing: false + ) + }() +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadAlertType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadAlertType.swift new file mode 100644 index 000000000..c2739b426 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadAlertType.swift @@ -0,0 +1,121 @@ +import MapboxDirections + +/// Represents different types of road alerts that can be displayed on a route. +/// Each alert type corresponds to a specific traffic condition or event that can affect the route. +public struct RoadAlertType: OptionSet { + public let rawValue: UInt + + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + /// Indicates a road alert for an accident on the route. + public static let accident = Self(rawValue: 1 << 0) + + /// Indicates a road alert for traffic congestion on the route. + public static let congestion = Self(rawValue: 1 << 1) + + /// Indicates a road alert for construction along the route. + public static let construction = Self(rawValue: 1 << 2) + + /// Indicates a road alert for a disabled vehicle on the route. + public static let disabledVehicle = Self(rawValue: 1 << 3) + + /// Indicates a road alert for lane restrictions on the route. + public static let laneRestriction = Self(rawValue: 1 << 4) + + /// Indicates a road alert related to mass transit on the route. + public static let massTransit = Self(rawValue: 1 << 5) + + /// Indicates a miscellaneous road alert on the route. + public static let miscellaneous = Self(rawValue: 1 << 6) + + /// Indicates a road alert for news impacting the route. + public static let otherNews = Self(rawValue: 1 << 7) + + /// Indicates a road alert for a planned event impacting the route. + public static let plannedEvent = Self(rawValue: 1 << 8) + + /// Indicates a road alert for a road closure on the route. + public static let roadClosure = Self(rawValue: 1 << 9) + + /// Indicates a road alert for hazardous road conditions on the route. + public static let roadHazard = Self(rawValue: 1 << 10) + + /// Indicates a road alert related to weather conditions affecting the route. + public static let weather = Self(rawValue: 1 << 11) + + /// A collection that includes all possible road alert types. + public static let all: Self = [ + .accident, + .congestion, + .construction, + .disabledVehicle, + .laneRestriction, + .massTransit, + .miscellaneous, + .otherNews, + .plannedEvent, + .roadClosure, + .roadHazard, + .weather, + ] +} + +extension RoadAlertType { + init?(roadObjectKind: RoadObject.Kind) { + switch roadObjectKind { + case .incident(let incident): + guard let roadAlertType = incident?.kind.flatMap(RoadAlertType.init) else { + return nil + } + self = roadAlertType + + case .tollCollection, + .borderCrossing, + .tunnel, + .serviceArea, + .restrictedArea, + .bridge, + .railroadCrossing, + .userDefined, + .ic, + .jct, + .undefined: + return nil + } + } +} + +extension RoadAlertType { + private init?(incident: Incident.Kind) { + switch incident { + case .accident: + self = .accident + case .congestion: + self = .congestion + case .construction: + self = .construction + case .disabledVehicle: + self = .disabledVehicle + case .laneRestriction: + self = .laneRestriction + case .massTransit: + self = .massTransit + case .miscellaneous: + self = .miscellaneous + case .otherNews: + self = .otherNews + case .plannedEvent: + self = .plannedEvent + case .roadClosure: + self = .roadClosure + case .roadHazard: + self = .roadHazard + case .weather: + self = .weather + case .undefined: + return nil + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadClassesSegment.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadClassesSegment.swift new file mode 100644 index 000000000..e2140f9be --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadClassesSegment.swift @@ -0,0 +1,6 @@ +import CoreLocation +import MapboxDirections + +/// A tuple that pairs an array of coordinates with assigned road classes +/// along these coordinates. +typealias RoadClassesSegment = ([CLLocationCoordinate2D], RoadClasses) diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Route.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Route.swift new file mode 100644 index 000000000..e2748034d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Route.swift @@ -0,0 +1,217 @@ +import CoreLocation +import MapboxDirections +import Turf + +extension Route { + /// Returns a polyline extending a given distance in either direction from a given maneuver along the route. + /// + /// The maneuver is identified by a leg index and step index, in case the route doubles back on itself. + /// + /// - parameter legIndex: Zero-based index of the leg containing the maneuver. + /// - parameter stepIndex: Zero-based index of the step containing the maneuver. + /// - parameter distance: Distance by which the resulting polyline extends in either direction from the maneuver. + /// - returns: A polyline whose length is twice `distance` and whose centroid is located at the maneuver. + func polylineAroundManeuver(legIndex: Int, stepIndex: Int, distance: CLLocationDistance) -> LineString { + let precedingLegs = legs.prefix(upTo: legIndex) + let precedingLegCoordinates = precedingLegs.flatMap(\.steps).flatMap { $0.shape?.coordinates ?? [] } + + let leg = legs[legIndex] + let precedingSteps = leg.steps.prefix(upTo: min(stepIndex, leg.steps.count)) + let precedingStepCoordinates = precedingSteps.compactMap { $0.shape?.coordinates }.reduce([], +) + let precedingPolyline = LineString((precedingLegCoordinates + precedingStepCoordinates).reversed()) + + let followingLegs = legs.dropFirst(legIndex + 1) + let followingLegCoordinates = followingLegs.flatMap(\.steps).flatMap { $0.shape?.coordinates ?? [] } + + let followingSteps = leg.steps.dropFirst(stepIndex) + let followingStepCoordinates = followingSteps.compactMap { $0.shape?.coordinates }.reduce([], +) + let followingPolyline = LineString(followingStepCoordinates + followingLegCoordinates) + + // After trimming, reverse the array so that the resulting polyline proceeds in a forward direction throughout. + let trimmedPrecedingCoordinates: [CLLocationCoordinate2D] = if precedingPolyline.coordinates.isEmpty { + [] + } else { + precedingPolyline.trimmed( + from: precedingPolyline.coordinates[0], + distance: distance + )!.coordinates.reversed() + } + // Omit the first coordinate, which is already contained in trimmedPrecedingCoordinates. + if followingPolyline.coordinates.isEmpty { + return LineString(trimmedPrecedingCoordinates) + } else { + return LineString(trimmedPrecedingCoordinates + followingPolyline.trimmed( + from: followingPolyline.coordinates[0], + distance: distance + )!.coordinates.dropFirst()) + } + } + + func restrictedRoadsFeatures() -> [Feature] { + guard shape != nil else { return [] } + + var hasRestriction = false + var features: [Feature] = [] + + for leg in legs { + let legRoadClasses = leg.roadClasses + + // The last coordinate of the preceding step, is shared with the first coordinate of the next step, we don't + // need both. + let legCoordinates: [CLLocationCoordinate2D] = leg.steps.enumerated() + .reduce([]) { allCoordinates, current in + let index = current.offset + let step = current.element + let stepCoordinates = step.shape!.coordinates + + return index == 0 ? stepCoordinates : allCoordinates + stepCoordinates.dropFirst() + } + + let mergedRoadClasses = legCoordinates.combined( + legRoadClasses, + combiningRoadClasses: .restricted + ) + + features.append(contentsOf: mergedRoadClasses.map { (roadClassesSegment: RoadClassesSegment) -> Feature in + var feature = Feature(geometry: .lineString(LineString(roadClassesSegment.0))) + feature.properties = [ + RestrictedRoadClassAttribute: .boolean(roadClassesSegment.1 == .restricted), + ] + + if !hasRestriction, roadClassesSegment.1 == .restricted { + hasRestriction = true + } + + return feature + }) + } + + return hasRestriction ? features : [] + } + + func congestionFeatures( + legIndex: Int? = nil, + rangesConfiguration: CongestionRangesConfiguration, + roadClassesWithOverriddenCongestionLevels: Set? = nil + ) -> [Feature] { + guard let coordinates = shape?.coordinates else { return [] } + var features: [Feature] = [] + + for (index, leg) in legs.enumerated() { + let legFeatures: [Feature] + let currentLegAttribute = (legIndex != nil) ? index == legIndex : true + + // The last coordinate of the preceding step, is shared with the first coordinate of the next step, we don't + // need both. + let legCoordinates: [CLLocationCoordinate2D] = leg.steps.enumerated() + .reduce([]) { allCoordinates, current in + let index = current.offset + let step = current.element + let stepCoordinates = step.shape!.coordinates + return index == 0 ? stepCoordinates : allCoordinates + stepCoordinates.dropFirst() + } + + if let congestionLevels = leg.resolveCongestionLevels(using: rangesConfiguration), + congestionLevels.count < coordinates.count + 2 + { + let mergedCongestionSegments = legCoordinates.combined( + congestionLevels, + streetsRoadClasses: leg.streetsRoadClasses, + roadClassesWithOverriddenCongestionLevels: roadClassesWithOverriddenCongestionLevels + ) + + legFeatures = mergedCongestionSegments.map { (congestionSegment: CongestionSegment) -> Feature in + var feature = Feature(geometry: .lineString(LineString(congestionSegment.0))) + feature.properties = [ + CongestionAttribute: .string(congestionSegment.1.rawValue), + CurrentLegAttribute: .boolean(currentLegAttribute), + ] + + return feature + } + } else { + var feature = Feature(geometry: .lineString(LineString(.init(coordinates: legCoordinates)))) + feature.properties = [ + CurrentLegAttribute: .boolean(currentLegAttribute), + ] + legFeatures = [feature] + } + + features.append(contentsOf: legFeatures) + } + + return features + } + + func leg(containing step: RouteStep) -> RouteLeg? { + return legs.first { $0.steps.contains(step) } + } + + var tollIntersections: [Intersection]? { + let allSteps = legs.flatMap { return $0.steps } + + let allIntersections = allSteps.flatMap { $0.intersections ?? [] } + let intersectionsWithTolls = allIntersections.filter { return $0.tollCollection != nil } + + return intersectionsWithTolls + } + + // returns the list of line segments along the route that fall within given bounding box. Returns nil if there are + // none. Line segments are defined by the route shape coordinates that lay within the bounding box + func shapes(within: Turf.BoundingBox) -> [LineString]? { + guard let coordinates = shape?.coordinates else { return nil } + var lines = [[CLLocationCoordinate2D]]() + var currentLine: [CLLocationCoordinate2D]? + for coordinate in coordinates { + // see if this coordinate lays within the bounds + if within.contains(coordinate) { + // if there is no current line segment then start one + if currentLine == nil { + currentLine = [CLLocationCoordinate2D]() + } + + // append the coordinate to the current line segment + currentLine?.append(coordinate) + } else { + // if there is a current line segment being built then finish it off and reset + if let currentLine { + lines.append(currentLine) + } + currentLine = nil + } + } + + // append any outstanding final segment + if let currentLine { + lines.append(currentLine) + } + currentLine = nil + + // return the segments as LineStrings + return lines.compactMap { coordinateList -> LineString? in + return LineString(coordinateList) + } + } + + /// Returns true if both the legIndex and stepIndex are valid in the route. + func containsStep(at legIndex: Int, stepIndex: Int) -> Bool { + return legs[safe: legIndex]?.steps.indices.contains(stepIndex) ?? false + } + + public var etaDistanceInfo: EtaDistanceInfo? { + .init(distance: distance, travelTime: expectedTravelTime) + } + + public func etaDistanceInfo(forLeg index: Int) -> EtaDistanceInfo? { + guard legs.indices.contains(index) else { return nil } + + let leg = legs[index] + return .init(distance: leg.distance, travelTime: leg.expectedTravelTime) + } +} + +extension RouteStep { + func intersects(_ boundingBox: Turf.BoundingBox) -> Bool { + return shape?.coordinates.contains(where: { boundingBox.contains($0) }) ?? false + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RouteDurationAnnotationTailPosition.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RouteDurationAnnotationTailPosition.swift new file mode 100644 index 000000000..acb4afbdf --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RouteDurationAnnotationTailPosition.swift @@ -0,0 +1,6 @@ +import Foundation + +enum RouteDurationAnnotationTailPosition: Int { + case leading + case trailing +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoutesPresentationStyle.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoutesPresentationStyle.swift new file mode 100644 index 000000000..acdce947f --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoutesPresentationStyle.swift @@ -0,0 +1,14 @@ +import MapboxMaps + +/// A style that will be used when presenting routes on top of a map view by calling +/// `NavigationMapView.showcase(_:routesPresentationStyle:animated:)`. +public enum RoutesPresentationStyle { + /// Only first route will be presented on a map view. + case main + + /// All routes will be presented on a map view. + /// + /// - parameter shouldFit: If `true` geometry of all routes will be used for camera transition. + /// If `false` geometry of only first route will be used. Defaults to `true`. + case all(shouldFit: Bool = true) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIColor++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIColor++.swift new file mode 100644 index 000000000..0aa120d0a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIColor++.swift @@ -0,0 +1,40 @@ +import MapboxMaps +import UIKit + +@_spi(MapboxInternal) +extension UIColor { + public class var defaultTintColor: UIColor { #colorLiteral(red: 0.1843137255, green: 0.4784313725, blue: 0.7764705882, alpha: 1) } + + public class var defaultRouteCasing: UIColor { .defaultTintColor } + public class var defaultRouteLayer: UIColor { #colorLiteral(red: 0.337254902, green: 0.6588235294, blue: 0.9843137255, alpha: 1) } + public class var defaultAlternateLine: UIColor { #colorLiteral(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) } + public class var defaultAlternateLineCasing: UIColor { #colorLiteral(red: 0.5019607843, green: 0.4980392157, blue: 0.5019607843, alpha: 1) } + public class var defaultManeuverArrowStroke: UIColor { .defaultRouteLayer } + public class var defaultManeuverArrow: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } + + public class var trafficUnknown: UIColor { defaultRouteLayer } + public class var trafficLow: UIColor { defaultRouteLayer } + public class var trafficModerate: UIColor { #colorLiteral(red: 1, green: 0.5843137255, blue: 0, alpha: 1) } + public class var trafficHeavy: UIColor { #colorLiteral(red: 1, green: 0.3019607843, blue: 0.3019607843, alpha: 1) } + public class var trafficSevere: UIColor { #colorLiteral(red: 0.5607843137, green: 0.1411764706, blue: 0.2784313725, alpha: 1) } + + public class var alternativeTrafficUnknown: UIColor { defaultAlternateLine } + public class var alternativeTrafficLow: UIColor { defaultAlternateLine } + public class var alternativeTrafficModerate: UIColor { #colorLiteral(red: 0.75, green: 0.63, blue: 0.53, alpha: 1.0) } + public class var alternativeTrafficHeavy: UIColor { #colorLiteral(red: 0.71, green: 0.51, blue: 0.51, alpha: 1.0) } + public class var alternativeTrafficSevere: UIColor { #colorLiteral(red: 0.71, green: 0.51, blue: 0.51, alpha: 1.0) } + + public class var defaultRouteRestrictedAreaColor: UIColor { #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) } + + public class var defaultRouteAnnotationColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } + public class var defaultSelectedRouteAnnotationColor: UIColor { #colorLiteral(red: 0.1882352941, green: 0.4470588235, blue: 0.9607843137, alpha: 1) } + + public class var defaultRouteAnnotationTextColor: UIColor { #colorLiteral(red: 0.01960784314, green: 0.02745098039, blue: 0.03921568627, alpha: 1) } + public class var defaultSelectedRouteAnnotationTextColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } + + public class var defaultRouteAnnotationMoreTimeTextColor: UIColor { #colorLiteral(red: 0.9215686275, green: 0.1450980392, blue: 0.1647058824, alpha: 1) } + public class var defaultRouteAnnotationLessTimeTextColor: UIColor { #colorLiteral(red: 0.03529411765, green: 0.6666666667, blue: 0.4549019608, alpha: 1) } + + public class var defaultWaypointColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } + public class var defaultWaypointStrokeColor: UIColor { #colorLiteral(red: 0.137254902, green: 0.1490196078, blue: 0.1764705882, alpha: 1) } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIFont.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIFont.swift new file mode 100644 index 000000000..15677385a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIFont.swift @@ -0,0 +1,8 @@ +import UIKit + +@_spi(MapboxInternal) +extension UIFont { + public class var defaultRouteAnnotationTextFont: UIFont { + .systemFont(ofSize: 18) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIImage++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIImage++.swift new file mode 100644 index 000000000..744f0c016 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIImage++.swift @@ -0,0 +1,47 @@ +import UIKit + +extension UIImage { + static let midpointMarkerImage = UIImage( + named: "midpoint_marker", + in: .mapboxNavigationUXCore, + compatibleWith: nil + )! + + convenience init?(color: UIColor, size: CGSize = CGSize(width: 1.0, height: 1.0)) { + let rect = CGRect(origin: .zero, size: size) + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0) + color.setFill() + UIRectFill(rect) + + let image = UIGraphicsGetImageFromCurrentImageContext() + defer { UIGraphicsEndImageContext() } + + guard let cgImage = image?.cgImage else { return nil } + self.init(cgImage: cgImage) + } + + func tint(_ tintColor: UIColor) -> UIImage { + let imageSize = size + let imageScale = scale + let contextBounds = CGRect(origin: .zero, size: imageSize) + + UIGraphicsBeginImageContextWithOptions(imageSize, false, imageScale) + + defer { UIGraphicsEndImageContext() } + + UIColor.black.setFill() + UIRectFill(contextBounds) + draw(at: .zero) + + guard let imageOverBlack = UIGraphicsGetImageFromCurrentImageContext() else { return self } + tintColor.setFill() + UIRectFill(contextBounds) + + imageOverBlack.draw(at: .zero, blendMode: .multiply, alpha: 1) + draw(at: .zero, blendMode: .destinationIn, alpha: 1) + + guard let finalImage = UIGraphicsGetImageFromCurrentImageContext() else { return self } + + return finalImage + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/VectorSource++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/VectorSource++.swift new file mode 100644 index 000000000..c78595738 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/VectorSource++.swift @@ -0,0 +1,84 @@ +import Foundation +import MapboxMaps + +extension VectorSource { + /// A dictionary associating known tile set identifiers with identifiers of source layers that contain road names. + static let roadLabelLayerIdentifiersByTileSetIdentifier = [ + "mapbox.mapbox-streets-v8": "road", + "mapbox.mapbox-streets-v7": "road_label", + ] + + /// Method, which returns a boolean value indicating whether the tile source is a supported version of the Mapbox + /// Streets source. + static func isMapboxStreets(_ identifiers: [String]) -> Bool { + return identifiers.contains("mapbox.mapbox-streets-v8") || identifiers.contains("mapbox.mapbox-streets-v7") + } + + /// An array of locales for which Mapbox Streets source v8 has a + /// [dedicated name + /// field](https://docs.mapbox.com/vector-tiles/reference/mapbox-streets-v8/#name-text--name_lang-code-text). + static let mapboxStreetsLocales = [ + "ar", + "de", + "en", + "es", + "fr", + "it", + "ja", + "ko", + "pt", + "ru", + "vi", + "zh-Hans", + "zh-Hant", + ].map(Locale.init(identifier:)) + + /// Returns the BCP 47 language tag supported by Mapbox Streets source v8 that is most preferred according to the + /// given preferences. + static func preferredMapboxStreetsLocalization(among preferences: [String]) -> String? { + let preferredLocales = preferences.map(Locale.init(identifier:)) + let acceptsEnglish = preferredLocales.contains { $0.languageCode == "en" } + var availableLocales = mapboxStreetsLocales + if !acceptsEnglish { + availableLocales.removeAll { $0.languageCode == "en" } + } + + let mostSpecificLanguage = Bundle.preferredLocalizations( + from: availableLocales.map(\.identifier), + forPreferences: preferences + ) + .max { $0.count > $1.count } + + // `Bundle.preferredLocalizations(from:forPreferences:)` is just returning the first localization it could find. + if let mostSpecificLanguage, + !preferredLocales + .contains(where: { $0.languageCode == Locale(identifier: mostSpecificLanguage).languageCode }) + { + return nil + } + + return mostSpecificLanguage + } + + /// Returns the locale supported by Mapbox Streets source v8 that is most preferred for the given locale. + /// + /// - parameter locale: The locale to match. To use the system’s preferred language, if supported, specify `nil`. + /// To use the local language, specify a locale with the identifier `mul`. + static func preferredMapboxStreetsLocale(for locale: Locale?) -> Locale? { + guard locale?.languageCode != "mul" else { + // FIXME: Unlocalization not yet implemented: https://github.com/mapbox/mapbox-maps-ios/issues/653 + return nil + } + + let preferences: [String] = if let locale { + [locale.identifier] + } else { + Locale.preferredLanguages + } + + guard let preferredLocalization = VectorSource.preferredMapboxStreetsLocalization(among: preferences) else { + return nil + } + return Locale(identifier: preferredLocalization) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift new file mode 100644 index 000000000..172b9b1b0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift @@ -0,0 +1,29 @@ +import Foundation + +extension AlternativeRoute { + /// Returns offset of the alternative route where it deviates from the main route. + func deviationOffset() -> Double { + guard let coordinates = route.shape?.coordinates, + !coordinates.isEmpty + else { + return 0 + } + + let splitGeometryIndex = alternativeRouteIntersectionIndices.routeGeometryIndex + + var totalDistance = 0.0 + var pointDistance: Double? = nil + for index in stride(from: coordinates.count - 1, to: 0, by: -1) { + let currCoordinate = coordinates[index] + let prevCoordinate = coordinates[index - 1] + totalDistance += currCoordinate.projectedDistance(to: prevCoordinate) + + if index == splitGeometryIndex + 1 { + pointDistance = totalDistance + } + } + guard let pointDistance, totalDistance != 0 else { return 0 } + + return (totalDistance - pointDistance) / totalDistance + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionColorsConfiguration.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionColorsConfiguration.swift new file mode 100644 index 000000000..d7a2ac02e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionColorsConfiguration.swift @@ -0,0 +1,76 @@ +import UIKit + +/// Configuration settings for congestion colors for the main and alternative routes. +public struct CongestionColorsConfiguration: Equatable, Sendable { + /// Color schema for the main route. + public var mainRouteColors: Colors + /// Color schema for the alternative route. + public var alternativeRouteColors: Colors + + /// Default colors configuration. + public static let `default` = CongestionColorsConfiguration( + mainRouteColors: .defaultMainRouteColors, + alternativeRouteColors: .defaultAlternativeRouteColors + ) + + /// Creates a new ``CongestionColorsConfiguration`` instance. + /// - Parameters: + /// - mainRouteColors: Color schema for the main route. + /// - alternativeRouteColors: Color schema for the alternative route. + public init( + mainRouteColors: CongestionColorsConfiguration.Colors, + alternativeRouteColors: CongestionColorsConfiguration.Colors + ) { + self.mainRouteColors = mainRouteColors + self.alternativeRouteColors = alternativeRouteColors + } +} + +extension CongestionColorsConfiguration { + /// Set of colors for different congestion levels. + public struct Colors: Equatable, Sendable { + /// Assigned color for `low` traffic. + public var low: UIColor + /// Assigned color for `moderate` traffic. + public var moderate: UIColor + /// Assigned color for `heavy` traffic. + public var heavy: UIColor + /// Assigned color for `severe` traffic. + public var severe: UIColor + /// Assigned color for `unknown` traffic. + public var unknown: UIColor + + /// Default color scheme for the main route. + public static let defaultMainRouteColors = Colors( + low: .trafficLow, + moderate: .trafficModerate, + heavy: .trafficHeavy, + severe: .trafficSevere, + unknown: .trafficUnknown + ) + + /// Default color scheme for the alternative route. + public static let defaultAlternativeRouteColors = Colors( + low: .alternativeTrafficLow, + moderate: .alternativeTrafficModerate, + heavy: .alternativeTrafficHeavy, + severe: .alternativeTrafficSevere, + unknown: .alternativeTrafficUnknown + ) + + /// Creates a new ``CongestionColorsConfiguration`` instance. + /// - Parameters: + /// - low: Assigned color for `low` traffic. + /// - moderate: Assigned color for `moderate` traffic. + /// - heavy: Assigned color for `heavy` traffic. + /// - severe: Assigned color for `severe` traffic. + /// - unknown: Assigned color for `unknown` traffic. + public init(low: UIColor, moderate: UIColor, heavy: UIColor, severe: UIColor, unknown: UIColor) { + self.low = low + self.moderate = moderate + self.heavy = heavy + self.severe = severe + self.unknown = unknown + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionConfiguration.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionConfiguration.swift new file mode 100644 index 000000000..0f2bd5475 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionConfiguration.swift @@ -0,0 +1,24 @@ +import Foundation + +/// Configuration for displaying roads congestion. +public struct CongestionConfiguration: Equatable, Sendable { + /// Colors schema used. + public var colors: CongestionColorsConfiguration + /// Range configuration for congestion. + public var ranges: CongestionRangesConfiguration + + /// Default configuration. + public static let `default` = CongestionConfiguration( + colors: .default, + ranges: .default + ) + + /// Creates a new ``CongestionConfiguration`` instance. + /// - Parameters: + /// - colors: Colors schema used. + /// - ranges: Range configuration for congestion. + public init(colors: CongestionColorsConfiguration, ranges: CongestionRangesConfiguration) { + self.colors = colors + self.ranges = ranges + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/FeatureIds.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/FeatureIds.swift new file mode 100644 index 000000000..994ef0f96 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/FeatureIds.swift @@ -0,0 +1,169 @@ + +enum FeatureIds { + private static let globalPrefix: String = "com.mapbox.navigation" + + struct RouteLine: Hashable, Sendable { + private static let prefix: String = "\(globalPrefix).route_line" + + static var main: Self { + .init(routeId: "\(prefix).main") + } + + static func alternative(idx: Int) -> Self { + .init(routeId: "\(prefix).alternative_\(idx)") + } + + let source: String + let main: String + let casing: String + + let restrictedArea: String + let restrictedAreaSource: String + let traversedRoute: String + + init(routeId: String) { + self.source = routeId + self.main = routeId + self.casing = "\(routeId).casing" + self.restrictedArea = "\(routeId).restricted_area" + self.restrictedAreaSource = "\(routeId).restricted_area" + self.traversedRoute = "\(routeId).traversed_route" + } + } + + struct ManeuverArrow { + private static let prefix: String = "\(globalPrefix).arrow" + + let id: String + let symbolId: String + let arrow: String + let arrowStroke: String + let arrowSymbol: String + let arrowSymbolCasing: String + let arrowSource: String + let arrowSymbolSource: String + let triangleTipImage: String + + init(arrowId: String) { + let id = "\(Self.prefix).\(arrowId)" + self.id = id + self.symbolId = "\(id).symbol" + self.arrow = "\(id)" + self.arrowStroke = "\(id).stroke" + self.arrowSymbol = "\(id).symbol" + self.arrowSymbolCasing = "\(id).symbol.casing" + self.arrowSource = "\(id).source" + self.arrowSymbolSource = "\(id).symbol_source" + self.triangleTipImage = "\(id).triangle_tip_image" + } + + static func nextArrow() -> Self { + .init(arrowId: "next") + } + } + + struct VoiceInstruction { + private static let prefix: String = "\(globalPrefix).voice_instruction" + + let featureId: String + let source: String + let layer: String + let circleLayer: String + + init() { + let id = "\(Self.prefix)" + self.featureId = id + self.source = "\(id).source" + self.layer = "\(id).layer" + self.circleLayer = "\(id).layer.circle" + } + + static var currentRoute: Self { + .init() + } + } + + struct IntersectionAnnotation { + private static let prefix: String = "\(globalPrefix).intersection_annotations" + + let featureId: String + let source: String + let layer: String + + let yieldSignImage: String + let stopSignImage: String + let railroadCrossingImage: String + let trafficSignalImage: String + + init() { + let id = "\(Self.prefix)" + self.featureId = id + self.source = "\(id).source" + self.layer = "\(id).layer" + self.yieldSignImage = "\(id).yield_sign" + self.stopSignImage = "\(id).stop_sign" + self.railroadCrossingImage = "\(id).railroad_crossing" + self.trafficSignalImage = "\(id).traffic_signal" + } + + static var currentRoute: Self { + .init() + } + } + + struct RouteAlertAnnotation { + private static let prefix: String = "\(globalPrefix).route_alert_annotations" + + let featureId: String + let source: String + let layer: String + + init() { + let id = "\(Self.prefix)" + self.featureId = id + self.source = "\(id).source" + self.layer = "\(id).layer" + } + + static var `default`: Self { + .init() + } + } + + struct RouteWaypoints { + private static let prefix: String = "\(globalPrefix)_waypoint" + + let featureId: String + let innerCircle: String + let markerIcon: String + let source: String + + init() { + self.featureId = "\(Self.prefix).route-waypoints" + self.innerCircle = "\(Self.prefix).innerCircleLayer" + self.markerIcon = "\(Self.prefix).symbolLayer" + self.source = "\(Self.prefix).source" + } + + static var `default`: Self { + .init() + } + } + + struct RouteAnnotation: Hashable, Sendable { + private static let prefix: String = "\(globalPrefix).route_line.annotation" + let layerId: String + + static var main: Self { + .init(annotationId: "\(prefix).main") + } + + static func alternative(index: Int) -> Self { + .init(annotationId: "\(prefix).alternative_\(index)") + } + + init(annotationId: String) { + self.layerId = annotationId + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift new file mode 100644 index 000000000..ae7c32362 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift @@ -0,0 +1,133 @@ +import _MapboxNavigationHelpers +import MapboxDirections +import MapboxMaps +import enum SwiftUI.ColorScheme + +extension RouteProgress { + func intersectionAnnotationsMapFeatures( + ids: FeatureIds.IntersectionAnnotation, + customizedLayerProvider: CustomizedLayerProvider + ) -> [any MapFeature] { + guard !routeIsComplete else { + return [] + } + + var featureCollection = FeatureCollection(features: []) + + let stepProgress = currentLegProgress.currentStepProgress + let intersectionIndex = stepProgress.intersectionIndex + let intersections = stepProgress.intersectionsIncludingUpcomingManeuverIntersection ?? [] + let stepIntersections = Array(intersections.dropFirst(intersectionIndex)) + + for intersection in stepIntersections { + if let feature = intersectionFeature(from: intersection, ids: ids) { + featureCollection.features.append(feature) + } + } + + let layers: [any Layer] = [ + with(SymbolLayer(id: ids.layer, source: ids.source)) { + $0.iconAllowOverlap = .constant(false) + $0.iconImage = .expression(Exp(.get) { + "imageName" + }) + }, + ] + return [ + GeoJsonMapFeature( + id: ids.featureId, + sources: [ + .init( + id: ids.source, + geoJson: .featureCollection(featureCollection) + ), + ], + customizeSource: { _, _ in }, + layers: layers.map { customizedLayerProvider.customizedLayer($0) }, + onBeforeAdd: { mapView in + Self.upsertIntersectionSymbolImages( + map: mapView.mapboxMap, + ids: ids + ) + }, + onUpdate: { mapView in + Self.upsertIntersectionSymbolImages( + map: mapView.mapboxMap, + ids: ids + ) + }, + onAfterRemove: { mapView in + do { + try Self.removeIntersectionSymbolImages( + map: mapView.mapboxMap, + ids: ids + ) + } catch { + Log.error( + "Failed to remove intersection annotation images with error \(error)", + category: .navigationUI + ) + } + } + ), + ] + } + + private func intersectionFeature( + from intersection: Intersection, + ids: FeatureIds.IntersectionAnnotation + ) -> Feature? { + var properties: JSONObject? + if intersection.yieldSign == true { + properties = ["imageName": .string(ids.yieldSignImage)] + } + if intersection.stopSign == true { + properties = ["imageName": .string(ids.stopSignImage)] + } + if intersection.railroadCrossing == true { + properties = ["imageName": .string(ids.railroadCrossingImage)] + } + if intersection.trafficSignal == true { + properties = ["imageName": .string(ids.trafficSignalImage)] + } + + guard let properties else { return nil } + + var feature = Feature(geometry: .point(Point(intersection.location))) + feature.properties = properties + return feature + } + + private static func upsertIntersectionSymbolImages( + map: MapboxMap, + ids: FeatureIds.IntersectionAnnotation + ) { + for (imageName, imageIdentifier) in imageNameToMapIdentifier(ids: ids) { + if let image = Bundle.module.image(named: imageName) { + map.provisionImage(id: imageIdentifier) { style in + try style.addImage(image, id: imageIdentifier) + } + } + } + } + + private static func removeIntersectionSymbolImages( + map: MapboxMap, + ids: FeatureIds.IntersectionAnnotation + ) throws { + for (_, imageIdentifier) in imageNameToMapIdentifier(ids: ids) { + try map.removeImage(withId: imageIdentifier) + } + } + + private static func imageNameToMapIdentifier( + ids: FeatureIds.IntersectionAnnotation + ) -> [String: String] { + return [ + "TrafficSignal": ids.trafficSignalImage, + "RailroadCrossing": ids.railroadCrossingImage, + "YieldSign": ids.yieldSignImage, + "StopSign": ids.stopSignImage, + ] + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift new file mode 100644 index 000000000..65a693dc0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift @@ -0,0 +1,131 @@ +import _MapboxNavigationHelpers +import Foundation +import MapboxDirections +@_spi(Experimental) import MapboxMaps + +extension Route { + func maneuverArrowMapFeatures( + ids: FeatureIds.ManeuverArrow, + cameraZoom: CGFloat, + legIndex: Int, stepIndex: Int, + config: MapStyleConfig, + customizedLayerProvider: CustomizedLayerProvider + ) -> [any MapFeature] { + guard containsStep(at: legIndex, stepIndex: stepIndex) + else { return [] } + + let triangleImage = Bundle.module.image(named: "triangle")!.withRenderingMode(.alwaysTemplate) + + var mapFeatures: [any MapFeature] = [] + + let step = legs[legIndex].steps[stepIndex] + let maneuverCoordinate = step.maneuverLocation + guard step.maneuverType != .arrive else { return [] } + + let metersPerPoint = Projection.metersPerPoint( + for: maneuverCoordinate.latitude, + zoom: cameraZoom + ) + + // TODO: Implement ability to change `shaftLength` depending on zoom level. + let shaftLength = max(min(50 * metersPerPoint, 50), 30) + let shaftPolyline = polylineAroundManeuver(legIndex: legIndex, stepIndex: stepIndex, distance: shaftLength) + + if shaftPolyline.coordinates.count > 1 { + let minimumZoomLevel = 14.5 + let shaftStrokeCoordinates = shaftPolyline.coordinates + let shaftDirection = shaftStrokeCoordinates[shaftStrokeCoordinates.count - 2] + .direction(to: shaftStrokeCoordinates.last!) + let point = Point(shaftStrokeCoordinates.last!) + + let layers: [any Layer] = [ + with(LineLayer(id: ids.arrow, source: ids.arrowSource)) { + $0.minZoom = Double(minimumZoomLevel) + $0.lineCap = .constant(.butt) + $0.lineJoin = .constant(.round) + $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.70)) + $0.lineColor = .constant(.init(config.maneuverArrowColor)) + $0.lineEmissiveStrength = .constant(1) + }, + with(LineLayer(id: ids.arrowStroke, source: ids.arrowSource)) { + $0.minZoom = Double(minimumZoomLevel) + $0.lineCap = .constant(.butt) + $0.lineJoin = .constant(.round) + $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.80)) + $0.lineColor = .constant(.init(config.maneuverArrowStrokeColor)) + $0.lineEmissiveStrength = .constant(1) + }, + with(SymbolLayer(id: ids.arrowSymbol, source: ids.arrowSymbolSource)) { + $0.minZoom = Double(minimumZoomLevel) + $0.iconImage = .constant(.name(ids.triangleTipImage)) + $0.iconColor = .constant(.init(config.maneuverArrowColor)) + $0.iconRotationAlignment = .constant(.map) + $0.iconRotate = .constant(.init(shaftDirection)) + $0.iconSize = .expression(Expression.routeLineWidthExpression(0.12)) + $0.iconAllowOverlap = .constant(true) + $0.iconEmissiveStrength = .constant(1) + }, + with(SymbolLayer(id: ids.arrowSymbolCasing, source: ids.arrowSymbolSource)) { + $0.minZoom = Double(minimumZoomLevel) + $0.iconImage = .constant(.name(ids.triangleTipImage)) + $0.iconColor = .constant(.init(config.maneuverArrowStrokeColor)) + $0.iconRotationAlignment = .constant(.map) + $0.iconRotate = .constant(.init(shaftDirection)) + $0.iconSize = .expression(Expression.routeLineWidthExpression(0.14)) + $0.iconAllowOverlap = .constant(true) + }, + ] + + mapFeatures.append( + GeoJsonMapFeature( + id: ids.id, + sources: [ + .init( + id: ids.arrowSource, + geoJson: .feature(Feature(geometry: .lineString(shaftPolyline))) + ), + .init( + id: ids.arrowSymbolSource, + geoJson: .feature(Feature(geometry: .point(point))) + ), + ], + customizeSource: { source, _ in + source.tolerance = 0.375 + }, + layers: layers.map { customizedLayerProvider.customizedLayer($0) }, + onBeforeAdd: { mapView in + mapView.mapboxMap.provisionImage(id: ids.triangleTipImage) { + try $0.addImage( + triangleImage, + id: ids.triangleTipImage, + sdf: true, + stretchX: [], + stretchY: [] + ) + } + }, + onUpdate: { mapView in + try with(mapView.mapboxMap) { + try $0.setLayerProperty( + for: ids.arrowSymbol, + property: "icon-rotate", + value: shaftDirection + ) + try $0.setLayerProperty( + for: ids.arrowSymbolCasing, + property: "icon-rotate", + value: shaftDirection + ) + } + }, + onAfterRemove: { mapView in + if mapView.mapboxMap.imageExists(withId: ids.triangleTipImage) { + try? mapView.mapboxMap.removeImage(withId: ids.triangleTipImage) + } + } + ) + ) + } + return mapFeatures + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift new file mode 100644 index 000000000..40063dbd1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift @@ -0,0 +1,285 @@ +import MapboxMaps +import UIKit + +final class ETAView: UIView { + private let label = { + let label = UILabel() + label.textAlignment = .left + return label + }() + + private var tail = UIView() + private let backgroundShape = CAShapeLayer() + private let mapStyleConfig: MapStyleConfig + + let textColor: UIColor + let baloonColor: UIColor + var padding = UIEdgeInsets(allEdges: 10) + var tailSize = 8.0 + var cornerRadius = 8.0 + + var text: String { + didSet { update() } + } + + var anchor: ViewAnnotationAnchor? { + didSet { setNeedsLayout() } + } + + convenience init( + eta: TimeInterval, + isSelected: Bool, + tollsHint: Bool?, + mapStyleConfig: MapStyleConfig + ) { + let viewLabel = DateComponentsFormatter.travelTimeString(eta, signed: false) + + let textColor: UIColor + let baloonColor: UIColor + if isSelected { + textColor = mapStyleConfig.routeAnnotationSelectedTextColor + baloonColor = mapStyleConfig.routeAnnotationSelectedColor + } else { + textColor = mapStyleConfig.routeAnnotationTextColor + baloonColor = mapStyleConfig.routeAnnotationColor + } + + self.init( + text: viewLabel, + tollsHint: tollsHint, + mapStyleConfig: mapStyleConfig, + textColor: textColor, + baloonColor: baloonColor + ) + } + + convenience init( + travelTimeDelta: TimeInterval, + tollsHint: Bool?, + mapStyleConfig: MapStyleConfig + ) { + let textColor: UIColor + let timeDelta: String + if abs(travelTimeDelta) >= 180 { + textColor = if travelTimeDelta > 0 { + mapStyleConfig.routeAnnotationMoreTimeTextColor + } else { + mapStyleConfig.routeAnnotationLessTimeTextColor + } + timeDelta = DateComponentsFormatter.travelTimeString( + travelTimeDelta, + signed: true + ) + } else { + textColor = mapStyleConfig.routeAnnotationTextColor + timeDelta = "SAME_TIME".localizedString( + value: "Similar ETA", + comment: "Alternatives selection note about equal travel time." + ) + } + + self.init( + text: timeDelta, + tollsHint: tollsHint, + mapStyleConfig: mapStyleConfig, + textColor: textColor, + baloonColor: mapStyleConfig.routeAnnotationColor + ) + } + + init( + text: String, + tollsHint: Bool?, + mapStyleConfig: MapStyleConfig, + textColor: UIColor = .darkText, + baloonColor: UIColor = .white + ) { + var viewLabel = text + switch tollsHint { + case .none: + label.numberOfLines = 1 + case .some(true): + label.numberOfLines = 2 + viewLabel += "\n" + "ROUTE_HAS_TOLLS".localizedString( + value: "Tolls", + comment: "Route callout label, indicating there are tolls on the route.") + if let symbol = Locale.current.currencySymbol { + viewLabel += " " + symbol + } + case .some(false): + label.numberOfLines = 2 + viewLabel += "\n" + "ROUTE_HAS_NO_TOLLS".localizedString( + value: "No Tolls", + comment: "Route callout label, indicating there are no tolls on the route.") + } + + self.text = viewLabel + self.textColor = textColor + self.baloonColor = baloonColor + self.mapStyleConfig = mapStyleConfig + super.init(frame: .zero) + layer.addSublayer(backgroundShape) + backgroundShape.shadowRadius = 1.4 + backgroundShape.shadowOffset = CGSize(width: 0, height: 0.7) + backgroundShape.shadowColor = UIColor.black.cgColor + backgroundShape.shadowOpacity = 0.3 + + addSubview(label) + + update() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var attributedText: NSAttributedString { + let text = NSMutableAttributedString( + attributedString: .labelText( + text, + font: mapStyleConfig.routeAnnotationTextFont, + color: textColor + ) + ) + return text + } + + private func update() { + backgroundShape.fillColor = baloonColor.cgColor + label.attributedText = attributedText + } + + struct Layout { + var label: CGRect + var bubble: CGRect + var size: CGSize + + init(availableSize: CGSize, text: NSAttributedString, tailSize: CGFloat, padding: UIEdgeInsets) { + let tailPadding = UIEdgeInsets(allEdges: tailSize) + + let textPadding = padding + tailPadding + UIEdgeInsets.zero + let textAvailableSize = availableSize - textPadding + let textSize = text.boundingRect( + with: textAvailableSize, + options: .usesLineFragmentOrigin, context: nil + ).size.roundedUp() + self.label = CGRect(padding: textPadding, size: textSize) + self.bubble = CGRect(padding: tailPadding, size: textSize + textPadding - tailPadding) + self.size = bubble.size + tailPadding + } + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + Layout(availableSize: size, text: attributedText, tailSize: tailSize, padding: padding).size + } + + override func layoutSubviews() { + super.layoutSubviews() + + let layout = Layout(availableSize: bounds.size, text: attributedText, tailSize: tailSize, padding: padding) + label.frame = layout.label + + let calloutPath = UIBezierPath.calloutPath( + size: bounds.size, + tailSize: tailSize, + cornerRadius: cornerRadius, + anchor: anchor ?? .center + ) + backgroundShape.path = calloutPath.cgPath + backgroundShape.frame = bounds + } +} + +extension UIEdgeInsets { + fileprivate init(allEdges value: CGFloat) { + self.init(top: value, left: value, bottom: value, right: value) + } +} + +extension NSAttributedString { + fileprivate static func labelText(_ string: String, font: UIFont, color: UIColor) -> NSAttributedString { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .left + let attributes = [ + NSAttributedString.Key.paragraphStyle: paragraphStyle, + .font: font, + .foregroundColor: color, + ] + return NSAttributedString(string: string, attributes: attributes) + } +} + +extension CGSize { + fileprivate func roundedUp() -> CGSize { + CGSize(width: width.rounded(.up), height: height.rounded(.up)) + } +} + +extension CGRect { + fileprivate init(padding: UIEdgeInsets, size: CGSize) { + self.init(origin: CGPoint(x: padding.left, y: padding.top), size: size) + } +} + +private func + (lhs: CGSize, rhs: UIEdgeInsets) -> CGSize { + return CGSize(width: lhs.width + rhs.left + rhs.right, height: lhs.height + rhs.top + rhs.bottom) +} + +private func - (lhs: CGSize, rhs: UIEdgeInsets) -> CGSize { + return CGSize(width: lhs.width - rhs.left - rhs.right, height: lhs.height - rhs.top - rhs.bottom) +} + +extension UIBezierPath { + fileprivate static func calloutPath( + size: CGSize, + tailSize: CGFloat, + cornerRadius: CGFloat, + anchor: ViewAnnotationAnchor + ) -> UIBezierPath { + let rect = CGRect(origin: .init(x: 0, y: 0), size: size) + let bubbleRect = rect.insetBy(dx: tailSize, dy: tailSize) + + let path = UIBezierPath( + roundedRect: bubbleRect, + cornerRadius: cornerRadius + ) + + let tailPath = UIBezierPath() + let p = tailSize + let h = size.height + let w = size.width + let r = cornerRadius + let tailPoints: [CGPoint] = switch anchor { + case .topLeft: + [CGPoint(x: 0, y: 0), CGPoint(x: p + r, y: p), CGPoint(x: p, y: p + r)] + case .top: + [CGPoint(x: w / 2, y: 0), CGPoint(x: w / 2 - p, y: p), CGPoint(x: w / 2 + p, y: p)] + case .topRight: + [CGPoint(x: w, y: 0), CGPoint(x: w - p, y: p + r), CGPoint(x: w - 3 * p, y: p)] + case .bottomLeft: + [CGPoint(x: 0, y: h), CGPoint(x: p, y: h - (p + r)), CGPoint(x: p + r, y: h - p)] + case .bottom: + [CGPoint(x: w / 2, y: h), CGPoint(x: w / 2 - p, y: h - p), CGPoint(x: w / 2 + p, y: h - p)] + case .bottomRight: + [CGPoint(x: w, y: h), CGPoint(x: w - (p + r), y: h - p), CGPoint(x: w - p, y: h - (p + r))] + case .left: + [CGPoint(x: 0, y: h / 2), CGPoint(x: p, y: h / 2 - p), CGPoint(x: p, y: h / 2 + p)] + case .right: + [CGPoint(x: w, y: h / 2), CGPoint(x: w - p, y: h / 2 - p), CGPoint(x: w - p, y: h / 2 + p)] + default: + [] + } + + for (i, point) in tailPoints.enumerated() { + if i == 0 { + tailPath.move(to: point) + } else { + tailPath.addLine(to: point) + } + } + tailPath.close() + path.append(tailPath) + return path + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift new file mode 100644 index 000000000..75d9b5c17 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift @@ -0,0 +1,139 @@ +import Foundation +import MapboxDirections +import MapboxMaps + +struct ETAViewsAnnotationFeature: MapFeature { + var id: String + + private let viewAnnotations: [ViewAnnotation] + + init( + for navigationRoutes: NavigationRoutes, + showMainRoute: Bool, + showAlternatives: Bool, + isRelative: Bool, + annotateAtManeuver: Bool, + mapStyleConfig: MapStyleConfig + ) { + let routesContainTolls = navigationRoutes.alternativeRoutes.contains { + ($0.route.tollIntersections?.count ?? 0) > 0 + } + var featureId = "" + + var annotations = [ViewAnnotation]() + if showMainRoute { + featureId += navigationRoutes.mainRoute.routeId.rawValue + let tollsHint = routesContainTolls ? navigationRoutes.mainRoute.route.containsTolls : nil + let etaView = ETAView( + eta: navigationRoutes.mainRoute.route.expectedTravelTime, + isSelected: true, + tollsHint: tollsHint, + mapStyleConfig: mapStyleConfig + ) + if let geometry = navigationRoutes.mainRoute.route.geometryForCallout() { + annotations.append( + ViewAnnotation( + annotatedFeature: .geometry(geometry), + view: etaView + ) + ) + } else { + annotations.append( + ViewAnnotation( + layerId: FeatureIds.RouteAnnotation.main.layerId, + view: etaView + ) + ) + } + } + if showAlternatives { + for (idx, alternativeRoute) in navigationRoutes.alternativeRoutes.enumerated() { + featureId += alternativeRoute.routeId.rawValue + let tollsHint = routesContainTolls ? alternativeRoute.route.containsTolls : nil + let etaView = if isRelative { + ETAView( + travelTimeDelta: alternativeRoute.expectedTravelTimeDelta, + tollsHint: tollsHint, + mapStyleConfig: mapStyleConfig + ) + } else { + ETAView( + eta: alternativeRoute.infoFromOrigin.duration, + isSelected: false, + tollsHint: tollsHint, + mapStyleConfig: mapStyleConfig + ) + } + let limit: Range + if annotateAtManeuver { + let deviationOffset = alternativeRoute.deviationOffset() + limit = (deviationOffset + 0.01)..<(deviationOffset + 0.05) + } else { + limit = 0.2..<0.8 + } + if let geometry = alternativeRoute.route.geometryForCallout(clampedTo: limit) { + annotations.append( + ViewAnnotation( + annotatedFeature: .geometry(geometry), + view: etaView + ) + ) + } else { + annotations.append( + ViewAnnotation( + layerId: FeatureIds.RouteAnnotation.alternative(index: idx).layerId, + view: etaView + ) + ) + } + } + } + annotations.forEach { + guard let etaView = $0.view as? ETAView else { return } + $0.setup(with: etaView) + } + self.id = featureId + self.viewAnnotations = annotations + } + + func add(to mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { + for annotation in viewAnnotations { + mapView.viewAnnotations.add(annotation) + } + } + + func remove(from mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { + viewAnnotations.forEach { $0.remove() } + } + + func update(oldValue: any MapFeature, in mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { + oldValue.remove(from: mapView, order: &order) + add(to: mapView, order: &order) + } +} + +extension Route { + fileprivate func geometryForCallout(clampedTo range: Range = 0.2..<0.8) -> Geometry? { + return shape?.trimmed( + from: distance * range.lowerBound, + to: distance * range.upperBound + )?.geometry + } + + fileprivate var containsTolls: Bool { + !(tollIntersections?.isEmpty ?? true) + } +} + +extension ViewAnnotation { + fileprivate func setup(with etaView: ETAView) { + ignoreCameraPadding = true + onAnchorChanged = { config in + etaView.anchor = config.anchor + } + variableAnchors = [ViewAnnotationAnchor.bottomLeft, .bottomRight, .topLeft, .topRight].map { + ViewAnnotationAnchorConfig(anchor: $0) + } + setNeedsUpdateSize() + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift new file mode 100644 index 000000000..2ddd6d67f --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift @@ -0,0 +1,239 @@ +import Foundation +import MapboxMaps +import Turf + +/// Simplifies source/layer/image managements for MapView +/// +/// ## Supported features: +/// +/// ### Layers +/// +/// Can be added/removed but not updated. Custom update logic can be performed using `onUpdate` callback. This +/// is done for performance reasons and to simplify implementation as map layers doesn't support equatable protocol. +/// If you want to update layers, you can consider assigning updated layer a new id. +/// +/// It there is only one source, layers will get it assigned automatically, overwise, layers should has source set +/// manually. +/// +/// ### Sources +/// +/// Sources can also be added/removed, but unlike layers, sources are always updated. +/// +/// +struct GeoJsonMapFeature: MapFeature { + struct Source { + let id: String + let geoJson: GeoJSONObject + } + + typealias LayerId = String + typealias SourceId = String + + let id: String + let sources: [SourceId: Source] + + let customizeSource: @MainActor (_ source: inout GeoJSONSource, _ id: String) -> Void + + let layers: [LayerId: any Layer] + + // MARK: Lifecycle callbacks + + let onBeforeAdd: @MainActor (_ mapView: MapView) -> Void + let onAfterAdd: @MainActor (_ mapView: MapView) -> Void + let onUpdate: @MainActor (_ mapView: MapView) throws -> Void + let onAfterUpdate: @MainActor (_ mapView: MapView) throws -> Void + let onAfterRemove: @MainActor (_ mapView: MapView) -> Void + + init( + id: String, + sources: [Source], + customizeSource: @escaping @MainActor (_: inout GeoJSONSource, _ id: String) -> Void, + layers: [any Layer], + onBeforeAdd: @escaping @MainActor (_: MapView) -> Void = { _ in }, + onAfterAdd: @escaping @MainActor (_: MapView) -> Void = { _ in }, + onUpdate: @escaping @MainActor (_: MapView) throws -> Void = { _ in }, + onAfterUpdate: @escaping @MainActor (_: MapView) throws -> Void = { _ in }, + onAfterRemove: @escaping @MainActor (_: MapView) -> Void = { _ in } + ) { + self.id = id + self.sources = Dictionary(uniqueKeysWithValues: sources.map { ($0.id, $0) }) + self.customizeSource = customizeSource + self.layers = Dictionary(uniqueKeysWithValues: layers.map { ($0.id, $0) }) + self.onBeforeAdd = onBeforeAdd + self.onAfterAdd = onAfterAdd + self.onUpdate = onUpdate + self.onAfterUpdate = onAfterUpdate + self.onAfterRemove = onAfterRemove + } + + // MARK: - MapFeature conformance + + @MainActor + func add(to mapView: MapView, order: inout MapLayersOrder) { + onBeforeAdd(mapView) + + let map: MapboxMap = mapView.mapboxMap + for (_, source) in sources { + addSource(source, to: map) + } + + for (_, var layer) in layers { + addLayer(&layer, to: map, order: &order) + } + + onAfterAdd(mapView) + } + + @MainActor + private func addLayer(_ layer: inout any Layer, to map: MapboxMap, order: inout MapLayersOrder) { + do { + if map.layerExists(withId: layer.id) { + try map.removeLayer(withId: layer.id) + } + order.insert(id: layer.id) + if let slot = order.slot(forId: layer.id), map.allSlotIdentifiers.contains(slot) { + layer.slot = slot + } + try map.addLayer(layer, layerPosition: order.position(forId: layer.id)) + } catch { + Log.error("Failed to add layer '\(layer.id)': \(error)", category: .navigationUI) + } + } + + @MainActor + private func addSource(_ source: Source, to map: MapboxMap) { + do { + if map.sourceExists(withId: source.id) { + map.updateGeoJSONSource( + withId: source.id, + geoJSON: source.geoJson + ) + } else { + var geoJsonSource = GeoJSONSource(id: source.id) + geoJsonSource.data = source.geoJson.sourceData + customizeSource(&geoJsonSource, source.id) + try map.addSource(geoJsonSource) + } + } catch { + Log.error("Failed to add source '\(source.id)': \(error)", category: .navigationUI) + } + } + + @MainActor + func update(oldValue: any MapFeature, in mapView: MapView, order: inout MapLayersOrder) { + guard let oldValue = oldValue as? Self else { + preconditionFailure("Incorrect type passed for oldValue") + } + + for (_, source) in sources { + guard mapView.mapboxMap.sourceExists(withId: source.id) + else { + // In case the map style was changed and the source is missing we're re-adding it back. + oldValue.remove(from: mapView, order: &order) + remove(from: mapView, order: &order) + add(to: mapView, order: &order) + return + } + } + + do { + try onUpdate(mapView) + let map: MapboxMap = mapView.mapboxMap + + let diff = diff(oldValue: oldValue, newValue: self) + for var addedLayer in diff.addedLayers { + addLayer(&addedLayer, to: map, order: &order) + } + for removedLayer in diff.removedLayers { + removeLayer(removedLayer, from: map, order: &order) + } + for addedSource in diff.addedSources { + addSource(addedSource, to: map) + } + for removedSource in diff.removedSources { + removeSource(removedSource.id, from: map) + } + + for (_, source) in sources { + mapView.mapboxMap.updateGeoJSONSource( + withId: source.id, + geoJSON: source.geoJson + ) + } + try onAfterUpdate(mapView) + } catch { + Log.error("Failed to update map feature '\(id)': \(error)", category: .navigationUI) + } + } + + @MainActor + func remove(from mapView: MapView, order: inout MapLayersOrder) { + let map: MapboxMap = mapView.mapboxMap + + for (_, layer) in layers { + removeLayer(layer, from: map, order: &order) + } + + for sourceId in sources.keys { + removeSource(sourceId, from: map) + } + + onAfterRemove(mapView) + } + + @MainActor + private func removeLayer(_ layer: any Layer, from map: MapboxMap, order: inout MapLayersOrder) { + guard map.layerExists(withId: layer.id) else { return } + do { + try map.removeLayer(withId: layer.id) + order.remove(id: layer.id) + } catch { + Log.error("Failed to remove layer '\(layer.id)': \(error)", category: .navigationUI) + } + } + + @MainActor + private func removeSource(_ sourceId: SourceId, from map: MapboxMap) { + if map.sourceExists(withId: sourceId) { + do { + try map.removeSource(withId: sourceId) + } catch { + Log.error("Failed to remove source '\(sourceId)': \(error)", category: .navigationUI) + } + } + } + + // MARK: Diff + + private struct Diff { + let addedLayers: [any Layer] + let removedLayers: [any Layer] + let addedSources: [Source] + let removedSources: [Source] + } + + private func diff(oldValue: Self, newValue: Self) -> Diff { + .init( + addedLayers: newValue.layers.filter { oldValue.layers[$0.key] == nil }.map(\.value), + removedLayers: oldValue.layers.filter { newValue.layers[$0.key] == nil }.map(\.value), + addedSources: newValue.sources.filter { oldValue.sources[$0.key] == nil }.map(\.value), + removedSources: oldValue.sources.filter { newValue.sources[$0.key] == nil }.map(\.value) + ) + } +} + +// MARK: Helpers + +extension GeoJSONObject { + /// Ported from MapboxMaps as the same var is internal in the SDK. + fileprivate var sourceData: GeoJSONSourceData { + switch self { + case .geometry(let geometry): + return .geometry(geometry) + case .feature(let feature): + return .feature(feature) + case .featureCollection(let collection): + return .featureCollection(collection) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift new file mode 100644 index 000000000..fcd831ada --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift @@ -0,0 +1,16 @@ +import Foundation +import MapboxMaps + +/// Something that can be added/removed/updated in MapboxMaps.MapView. +/// +/// Use ``MapFeaturesStore`` to manage a set of features. +protocol MapFeature { + var id: String { get } + + @MainActor + func add(to mapView: MapView, order: inout MapLayersOrder) + @MainActor + func remove(from mapView: MapView, order: inout MapLayersOrder) + @MainActor + func update(oldValue: MapFeature, in mapView: MapView, order: inout MapLayersOrder) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift new file mode 100644 index 000000000..1f5cb49fb --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift @@ -0,0 +1,117 @@ +import Foundation +import MapboxMaps + +/// A store for ``MapFeature``s. +/// +/// It handle style reload by re-adding currently active features (make sure you call `styleLoaded` method). +/// Use `update(using:)` method to provide a new snapshot of features that are managed by this store. The store will +/// handle updates/removes/additions to the map view. +@MainActor +final class MapFeaturesStore { + private struct Features: Sequence { + private var features: [String: any MapFeature] = [:] + + func makeIterator() -> some IteratorProtocol { + features.values.makeIterator() + } + + subscript(_ id: String) -> (any MapFeature)? { + features[id] + } + + mutating func insert(_ feature: any MapFeature) { + features[feature.id] = feature + } + + mutating func remove(_ feature: any MapFeature) -> (any MapFeature)? { + features.removeValue(forKey: feature.id) + } + + mutating func removeAll() -> some Sequence { + let allFeatures = features.values + features = [:] + return allFeatures + } + } + + private let mapView: MapView + private var styleLoadSubscription: MapboxMaps.Cancelable? + private var features: Features = .init() + + private var currentStyleLoaded: Bool = false + private var currentStyleUri: StyleURI? + + private var styleLoaded: Bool { + if currentStyleUri != mapView.mapboxMap.styleURI { + currentStyleLoaded = false + } + return currentStyleLoaded + } + + init(mapView: MapView) { + self.mapView = mapView + self.currentStyleUri = mapView.mapboxMap.styleURI + self.currentStyleLoaded = mapView.mapboxMap.isStyleLoaded + } + + func deactivate(order: inout MapLayersOrder) { + styleLoadSubscription?.cancel() + guard styleLoaded else { return } + features.forEach { $0.remove(from: mapView, order: &order) } + } + + func update(using allFeatures: [any MapFeature]?, order: inout MapLayersOrder) { + guard let allFeatures, !allFeatures.isEmpty else { + removeAll(order: &order); return + } + + let newFeatureIds = Set(allFeatures.map(\.id)) + for existingFeature in features where !newFeatureIds.contains(existingFeature.id) { + remove(existingFeature, order: &order) + } + + for feature in allFeatures { + update(feature, order: &order) + } + } + + private func removeAll(order: inout MapLayersOrder) { + let allFeatures = features.removeAll() + guard styleLoaded else { return } + + for feature in allFeatures { + feature.remove(from: mapView, order: &order) + } + } + + private func update(_ feature: any MapFeature, order: inout MapLayersOrder) { + defer { + features.insert(feature) + } + + guard styleLoaded else { return } + + if let oldFeature = features[feature.id] { + feature.update(oldValue: oldFeature, in: mapView, order: &order) + } else { + feature.add(to: mapView, order: &order) + } + } + + private func remove(_ feature: some MapFeature, order: inout MapLayersOrder) { + guard let removeFeature = features.remove(feature) else { return } + + if styleLoaded { + removeFeature.remove(from: mapView, order: &order) + } + } + + func styleLoaded(order: inout MapLayersOrder) { + currentStyleUri = mapView.mapboxMap.styleURI + currentStyleLoaded = true + + for feature in features { + feature.add(to: mapView, order: &order) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift new file mode 100644 index 000000000..830f23d92 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift @@ -0,0 +1,38 @@ +import MapboxMaps + +extension MapboxMap { + /// Adds image to style if it doesn't exist already and log any errors that occur. + func provisionImage(id: String, _ addImageToMap: (MapboxMap) throws -> Void) { + if !imageExists(withId: id) { + do { + try addImageToMap(self) + } catch { + Log.error("Failed to add image (id: \(id)) to style with error \(error)", category: .navigationUI) + } + } + } + + func setRouteLineOffset( + _ offset: Double, + for routeLineIds: FeatureIds.RouteLine + ) { + guard offset >= 0.0 else { return } + do { + let layerIds: [String] = [ + routeLineIds.main, + routeLineIds.casing, + routeLineIds.restrictedArea, + ] + + for layerId in layerIds where layerExists(withId: layerId) { + try setLayerProperty( + for: layerId, + property: "line-trim-offset", + value: [0.0, Double.minimum(1.0, offset)] + ) + } + } catch { + Log.error("Failed to update route line gradient with error: \(error)", category: .navigationUI) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapLayersOrder.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapLayersOrder.swift new file mode 100644 index 000000000..bdb23e842 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapLayersOrder.swift @@ -0,0 +1,260 @@ +import _MapboxNavigationHelpers +import MapboxMaps + +/// Allows to order layers with easy by defining order rules and then query order for any added layer. +struct MapLayersOrder { + @resultBuilder + enum Builder { + static func buildPartialBlock(first rule: Rule) -> [Rule] { + [rule] + } + + static func buildPartialBlock(first slottedRules: SlottedRules) -> [Rule] { + slottedRules.rules + } + + static func buildPartialBlock(accumulated rules: [Rule], next rule: Rule) -> [Rule] { + with(rules) { + $0.append(rule) + } + } + + static func buildPartialBlock(accumulated rules: [Rule], next slottedRules: SlottedRules) -> [Rule] { + rules + slottedRules.rules + } + } + + struct SlottedRules { + let rules: [MapLayersOrder.Rule] + + init(_ slot: Slot?, @MapLayersOrder.Builder rules: () -> [Rule]) { + self.rules = rules().map { rule in + with(rule) { $0.slot = slot } + } + } + } + + struct Rule { + struct MatchPredicate { + let block: (String) -> Bool + + static func hasPrefix(_ prefix: String) -> Self { + .init { + $0.hasPrefix(prefix) + } + } + + static func contains(_ substring: String) -> Self { + .init { + $0.contains(substring) + } + } + + static func exact(_ id: String) -> Self { + .init { + $0 == id + } + } + + static func any(of ids: any Sequence) -> Self { + let set = Set(ids) + return .init { + set.contains($0) + } + } + } + + struct OrderedAscendingComparator { + let block: (_ lhs: String, _ rhs: String) -> Bool + + static func constant(_ value: Bool) -> Self { + .init { _, _ in + value + } + } + + static func order(_ ids: [String]) -> Self { + return .init { lhs, rhs in + guard let lhsIndex = ids.firstIndex(of: lhs), + let rhsIndex = ids.firstIndex(of: rhs) else { return true } + return lhsIndex < rhsIndex + } + } + } + + let matches: (String) -> Bool + let isOrderedAscending: (_ lhs: String, _ rhs: String) -> Bool + var slot: Slot? + + init( + predicate: MatchPredicate, + isOrderedAscending: OrderedAscendingComparator + ) { + self.matches = predicate.block + self.isOrderedAscending = isOrderedAscending.block + } + + static func hasPrefix( + _ prefix: String, + isOrderedAscending: OrderedAscendingComparator = .constant(true) + ) -> Rule { + Rule(predicate: .hasPrefix(prefix), isOrderedAscending: isOrderedAscending) + } + + static func contains( + _ substring: String, + isOrderedAscending: OrderedAscendingComparator = .constant(true) + ) -> Rule { + Rule(predicate: .contains(substring), isOrderedAscending: isOrderedAscending) + } + + static func exact( + _ id: String, + isOrderedAscending: OrderedAscendingComparator = .constant(true) + ) -> Rule { + Rule(predicate: .exact(id), isOrderedAscending: isOrderedAscending) + } + + static func orderedIds(_ ids: [String]) -> Rule { + return Rule( + predicate: .any(of: ids), + isOrderedAscending: .order(ids) + ) + } + + func slotted(_ slot: Slot) -> Self { + with(self) { + $0.slot = slot + } + } + } + + /// Ids that are managed by map style. + private var styleIds: [String] = [] + /// Ids that are managed by SDK. + private var customIds: Set = [] + /// Merged `styleIds` and `customIds` in order defined by rules. + private var orderedIds: [String] = [] + /// A map from id to position in `orderedIds` to speed up `position(forId:)` query. + private var orderedIdsIndices: [String: Int] = [:] + private var idToSlot: [String: Slot] = [:] + /// Ordered list of rules that define order. + private let rules: [Rule] + + /// Used for styles with no slots support. + private let legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? + + init( + @MapLayersOrder.Builder builder: () -> [Rule], + legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? + ) { + self.rules = builder() + self.legacyPosition = legacyPosition + } + + /// Inserts a new id and makes it possible to use it in `position(forId:)` method. + mutating func insert(id: String) { + customIds.insert(id) + + guard let ruleIndex = rules.firstIndex(where: { $0.matches(id) }) else { + orderedIds.append(id) + orderedIdsIndices[id] = orderedIds.count - 1 + return + } + + func binarySearch() -> Int { + var left = 0 + var right = orderedIds.count + + while left < right { + let mid = left + (right - left) / 2 + if let currentRuleIndex = rules.firstIndex(where: { $0.matches(orderedIds[mid]) }) { + if currentRuleIndex > ruleIndex { + right = mid + } else if currentRuleIndex == ruleIndex { + if !rules[ruleIndex].isOrderedAscending(orderedIds[mid], id) { + right = mid + } else { + left = mid + 1 + } + } else { + left = mid + 1 + } + } else { + right = mid + } + } + return left + } + + let insertionIndex = binarySearch() + orderedIds.insert(id, at: insertionIndex) + + // Update the indices of the elements after the insertion point + for index in insertionIndex.. LayerPosition? { + if let legacyPosition { + return legacyPosition(id) + } + + guard let index = orderedIdsIndices[id] else { return nil } + let belowId = index == 0 ? nil : orderedIds[index - 1] + let aboveId = index == orderedIds.count - 1 ? nil : orderedIds[index + 1] + + if let belowId { + return .above(belowId) + } else if let aboveId { + return .below(aboveId) + } else { + return nil + } + } + + func slot(forId id: String) -> Slot? { + idToSlot[id] + } + + private func rule(matching id: String) -> Rule? { + rules.first { rule in + rule.matches(id) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift new file mode 100644 index 000000000..d4eeabeca --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift @@ -0,0 +1,537 @@ +import Combine +import MapboxDirections +@_spi(Experimental) import MapboxMaps +import enum SwiftUI.ColorScheme +import UIKit + +struct CustomizedLayerProvider { + var customizedLayer: (Layer) -> Layer +} + +struct MapStyleConfig: Equatable { + var routeCasingColor: UIColor + var routeAlternateCasingColor: UIColor + var routeRestrictedAreaColor: UIColor + var traversedRouteColor: UIColor? + var maneuverArrowColor: UIColor + var maneuverArrowStrokeColor: UIColor + + var routeAnnotationSelectedColor: UIColor + var routeAnnotationColor: UIColor + var routeAnnotationSelectedTextColor: UIColor + var routeAnnotationTextColor: UIColor + var routeAnnotationMoreTimeTextColor: UIColor + var routeAnnotationLessTimeTextColor: UIColor + var routeAnnotationTextFont: UIFont + + var routeLineTracksTraversal: Bool + var isRestrictedAreaEnabled: Bool + var showsTrafficOnRouteLine: Bool + var showsAlternatives: Bool + var showsIntermediateWaypoints: Bool + var occlusionFactor: Value? + var congestionConfiguration: CongestionConfiguration + + var waypointColor: UIColor + var waypointStrokeColor: UIColor +} + +/// Manages all the sources/layers used in NavigationMap. +@MainActor +final class NavigationMapStyleManager { + private let mapView: MapView + private var lifetimeSubscriptions: Set = [] + private var layersOrder: MapLayersOrder + private var layerIds: [String] + + var customizedLayerProvider: CustomizedLayerProvider = .init { $0 } + var customRouteLineLayerPosition: LayerPosition? + + private let routeFeaturesStore: MapFeaturesStore + private let waypointFeaturesStore: MapFeaturesStore + private let arrowFeaturesStore: MapFeaturesStore + private let voiceInstructionFeaturesStore: MapFeaturesStore + private let intersectionAnnotationsFeaturesStore: MapFeaturesStore + private let routeAnnotationsFeaturesStore: MapFeaturesStore + private let routeAlertsFeaturesStore: MapFeaturesStore + + init(mapView: MapView, customRouteLineLayerPosition: LayerPosition?) { + self.mapView = mapView + self.layersOrder = Self.makeMapLayersOrder( + with: mapView, + customRouteLineLayerPosition: customRouteLineLayerPosition + ) + self.layerIds = mapView.mapboxMap.allLayerIdentifiers.map(\.id) + self.routeFeaturesStore = .init(mapView: mapView) + self.waypointFeaturesStore = .init(mapView: mapView) + self.arrowFeaturesStore = .init(mapView: mapView) + self.voiceInstructionFeaturesStore = .init(mapView: mapView) + self.intersectionAnnotationsFeaturesStore = .init(mapView: mapView) + self.routeAnnotationsFeaturesStore = .init(mapView: mapView) + self.routeAlertsFeaturesStore = .init(mapView: mapView) + + mapView.mapboxMap.onStyleLoaded.sink { [weak self] _ in + self?.onStyleLoaded() + }.store(in: &lifetimeSubscriptions) + } + + func onStyleLoaded() { + // MapsSDK removes all layers when a style is loaded, so we have to recreate MapLayersOrder. + layersOrder = Self.makeMapLayersOrder(with: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) + layerIds = mapView.mapboxMap.allLayerIdentifiers.map(\.id) + layersOrder.setStyleIds(layerIds) + + routeFeaturesStore.styleLoaded(order: &layersOrder) + waypointFeaturesStore.styleLoaded(order: &layersOrder) + arrowFeaturesStore.styleLoaded(order: &layersOrder) + voiceInstructionFeaturesStore.styleLoaded(order: &layersOrder) + intersectionAnnotationsFeaturesStore.styleLoaded(order: &layersOrder) + routeAnnotationsFeaturesStore.styleLoaded(order: &layersOrder) + routeAlertsFeaturesStore.styleLoaded(order: &layersOrder) + } + + func updateRoutes( + _ routes: NavigationRoutes, + config: MapStyleConfig, + featureProvider: RouteLineFeatureProvider + ) { + routeFeaturesStore.update( + using: routeLineMapFeatures( + routes: routes, + config: config, + featureProvider: featureProvider, + customizedLayerProvider: customizedLayerProvider + ), + order: &layersOrder + ) + } + + func updateWaypoints( + route: Route, + legIndex: Int, + config: MapStyleConfig, + featureProvider: WaypointFeatureProvider + ) { + let waypoints = route.waypointsMapFeature( + mapView: mapView, + legIndex: legIndex, + config: config, + featureProvider: featureProvider, + customizedLayerProvider: customizedLayerProvider + ) + waypointFeaturesStore.update( + using: waypoints.map { [$0] } ?? [], + order: &layersOrder + ) + } + + func updateArrows( + route: Route, + legIndex: Int, + stepIndex: Int, + config: MapStyleConfig + ) { + guard route.containsStep(at: legIndex, stepIndex: stepIndex) + else { + removeArrows(); return + } + + arrowFeaturesStore.update( + using: route.maneuverArrowMapFeatures( + ids: .nextArrow(), + cameraZoom: mapView.mapboxMap.cameraState.zoom, + legIndex: legIndex, + stepIndex: stepIndex, + config: config, + customizedLayerProvider: customizedLayerProvider + ), + order: &layersOrder + ) + } + + func updateVoiceInstructions(route: Route) { + voiceInstructionFeaturesStore.update( + using: route.voiceInstructionMapFeatures( + ids: .init(), + customizedLayerProvider: customizedLayerProvider + ), + order: &layersOrder + ) + } + + func updateIntersectionAnnotations(routeProgress: RouteProgress) { + intersectionAnnotationsFeaturesStore.update( + using: routeProgress.intersectionAnnotationsMapFeatures( + ids: .currentRoute, + customizedLayerProvider: customizedLayerProvider + ), + order: &layersOrder + ) + } + + func updateRouteAnnotations( + navigationRoutes: NavigationRoutes, + annotationKinds: Set, + config: MapStyleConfig + ) { + routeAnnotationsFeaturesStore.update( + using: navigationRoutes.routeDurationMapFeatures( + annotationKinds: annotationKinds, + config: config + ), + order: &layersOrder + ) + } + + func updateRouteAlertsAnnotations( + navigationRoutes: NavigationRoutes, + excludedRouteAlertTypes: RoadAlertType, + distanceTraveled: CLLocationDistance = 0.0 + ) { + routeAlertsFeaturesStore.update( + using: navigationRoutes.routeAlertsAnnotationsMapFeatures( + ids: .default, + distanceTraveled: distanceTraveled, + customizedLayerProvider: customizedLayerProvider, + excludedRouteAlertTypes: excludedRouteAlertTypes + ), + order: &layersOrder + ) + } + + func updateFreeDriveAlertsAnnotations( + roadObjects: [RoadObjectAhead], + excludedRouteAlertTypes: RoadAlertType, + distanceTraveled: CLLocationDistance = 0.0 + ) { + guard !roadObjects.isEmpty else { + return removeRoadAlertsAnnotations() + } + routeAlertsFeaturesStore.update( + using: roadObjects.routeAlertsAnnotationsMapFeatures( + ids: .default, + distanceTraveled: distanceTraveled, + customizedLayerProvider: customizedLayerProvider, + excludedRouteAlertTypes: excludedRouteAlertTypes + ), + order: &layersOrder + ) + } + + func removeRoutes() { + routeFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeWaypoints() { + waypointFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeArrows() { + arrowFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeVoiceInstructions() { + voiceInstructionFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeIntersectionAnnotations() { + intersectionAnnotationsFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeRouteAnnotations() { + routeAnnotationsFeaturesStore.update(using: nil, order: &layersOrder) + } + + private func removeRoadAlertsAnnotations() { + routeAlertsFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeAllFeatures() { + removeRoutes() + removeWaypoints() + removeArrows() + removeVoiceInstructions() + removeIntersectionAnnotations() + removeRouteAnnotations() + removeRoadAlertsAnnotations() + } + + private func routeLineMapFeatures( + routes: NavigationRoutes, + config: MapStyleConfig, + featureProvider: RouteLineFeatureProvider, + customizedLayerProvider: CustomizedLayerProvider + ) -> [any MapFeature] { + var features: [any MapFeature] = [] + + features.append(contentsOf: routes.mainRoute.route.routeLineMapFeatures( + ids: .main, + offset: 0, + isSoftGradient: true, + isAlternative: false, + config: config, + featureProvider: featureProvider, + customizedLayerProvider: customizedLayerProvider + )) + + if config.showsAlternatives { + for (idx, alternativeRoute) in routes.alternativeRoutes.enumerated() { + let deviationOffset = alternativeRoute.deviationOffset() + features.append(contentsOf: alternativeRoute.route.routeLineMapFeatures( + ids: .alternative(idx: idx), + offset: deviationOffset, + isSoftGradient: true, + isAlternative: true, + config: config, + featureProvider: featureProvider, + customizedLayerProvider: customizedLayerProvider + )) + } + } + + return features + } + + func setRouteLineOffset( + _ offset: Double, + for routeLineIds: FeatureIds.RouteLine + ) { + mapView.mapboxMap.setRouteLineOffset(offset, for: routeLineIds) + } + + private static func makeMapLayersOrder( + with mapView: MapView, + customRouteLineLayerPosition: LayerPosition? + ) -> MapLayersOrder { + let alternative_0_ids = FeatureIds.RouteLine.alternative(idx: 0) + let alternative_1_ids = FeatureIds.RouteLine.alternative(idx: 1) + let mainLineIds = FeatureIds.RouteLine.main + let arrowIds = FeatureIds.ManeuverArrow.nextArrow() + let waypointIds = FeatureIds.RouteWaypoints.default + let voiceInstructionIds = FeatureIds.VoiceInstruction.currentRoute + let intersectionIds = FeatureIds.IntersectionAnnotation.currentRoute + let routeAlertIds = FeatureIds.RouteAlertAnnotation.default + typealias R = MapLayersOrder.Rule + typealias SlottedRules = MapLayersOrder.SlottedRules + + let allSlotIdentifiers = mapView.mapboxMap.allSlotIdentifiers + let containsMiddleSlot = Slot.middle.map(allSlotIdentifiers.contains) ?? false + let legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? = containsMiddleSlot ? nil : { + legacyLayerPosition(for: $0, mapView: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) + } + + return MapLayersOrder( + builder: { + SlottedRules(.middle) { + R.orderedIds([ + alternative_0_ids.casing, + alternative_0_ids.main, + ]) + R.orderedIds([ + alternative_1_ids.casing, + alternative_1_ids.main, + ]) + R.orderedIds([ + mainLineIds.traversedRoute, + mainLineIds.casing, + mainLineIds.main, + ]) + R.orderedIds([ + arrowIds.arrowStroke, + arrowIds.arrow, + arrowIds.arrowSymbolCasing, + arrowIds.arrowSymbol, + ]) + R.orderedIds([ + alternative_0_ids.restrictedArea, + alternative_1_ids.restrictedArea, + mainLineIds.restrictedArea, + ]) + /// To show on top of arrows + R.hasPrefix("poi") + R.orderedIds([ + voiceInstructionIds.layer, + voiceInstructionIds.circleLayer, + ]) + } + // Setting the top position on the map. We cannot explicitly set `.top` position because `.top` + // renders behind Place and Transit labels + SlottedRules(nil) { + R.orderedIds([ + intersectionIds.layer, + routeAlertIds.layer, + waypointIds.innerCircle, + waypointIds.markerIcon, + NavigationMapView.LayerIdentifier.puck2DLayer, + NavigationMapView.LayerIdentifier.puck3DLayer, + ]) + } + }, + legacyPosition: legacyPosition + ) + } + + private static func legacyLayerPosition( + for layerIdentifier: String, + mapView: MapView, + customRouteLineLayerPosition: LayerPosition? + ) -> MapboxMaps.LayerPosition? { + let mainLineIds = FeatureIds.RouteLine.main + if layerIdentifier.hasPrefix(mainLineIds.main), + let customRouteLineLayerPosition, + !mapView.mapboxMap.allLayerIdentifiers.contains(where: { $0.id.hasPrefix(mainLineIds.main) }) + { + return customRouteLineLayerPosition + } + + let alternative_0_ids = FeatureIds.RouteLine.alternative(idx: 0) + let alternative_1_ids = FeatureIds.RouteLine.alternative(idx: 1) + let arrowIds = FeatureIds.ManeuverArrow.nextArrow() + let waypointIds = FeatureIds.RouteWaypoints.default + let voiceInstructionIds = FeatureIds.VoiceInstruction.currentRoute + let intersectionIds = FeatureIds.IntersectionAnnotation.currentRoute + let routeAlertIds = FeatureIds.RouteAlertAnnotation.default + + let lowermostSymbolLayers: [String] = [ + alternative_0_ids.casing, + alternative_0_ids.main, + alternative_1_ids.casing, + alternative_1_ids.main, + mainLineIds.traversedRoute, + mainLineIds.casing, + mainLineIds.main, + mainLineIds.restrictedArea, + ].compactMap { $0 } + let aboveRoadLayers: [String] = [ + arrowIds.arrowStroke, + arrowIds.arrow, + arrowIds.arrowSymbolCasing, + arrowIds.arrowSymbol, + intersectionIds.layer, + routeAlertIds.layer, + waypointIds.innerCircle, + waypointIds.markerIcon, + ] + let uppermostSymbolLayers: [String] = [ + voiceInstructionIds.layer, + voiceInstructionIds.circleLayer, + NavigationMapView.LayerIdentifier.puck2DLayer, + NavigationMapView.LayerIdentifier.puck3DLayer, + ] + let isLowermostLayer = lowermostSymbolLayers.contains(layerIdentifier) + let isAboveRoadLayer = aboveRoadLayers.contains(layerIdentifier) + let allAddedLayers: [String] = lowermostSymbolLayers + aboveRoadLayers + uppermostSymbolLayers + + var layerPosition: MapboxMaps.LayerPosition? + var lowerLayers = Set() + var upperLayers = Set() + var targetLayer: String? + + if let index = allAddedLayers.firstIndex(of: layerIdentifier) { + lowerLayers = Set(allAddedLayers.prefix(upTo: index)) + if allAddedLayers.indices.contains(index + 1) { + upperLayers = Set(allAddedLayers.suffix(from: index + 1)) + } + } + + var foundAboveLayer = false + for layerInfo in mapView.mapboxMap.allLayerIdentifiers.reversed() { + if lowerLayers.contains(layerInfo.id) { + // find the topmost layer that should be below the layerIdentifier. + if !foundAboveLayer { + layerPosition = .above(layerInfo.id) + foundAboveLayer = true + } + } else if upperLayers.contains(layerInfo.id) { + // find the bottommost layer that should be above the layerIdentifier. + layerPosition = .below(layerInfo.id) + } else if isLowermostLayer { + // find the topmost non symbol layer for layerIdentifier in lowermostSymbolLayers. + if targetLayer == nil, + layerInfo.type.rawValue != "symbol", + let sourceLayer = mapView.mapboxMap.layerProperty(for: layerInfo.id, property: "source-layer") + .value as? String, + !sourceLayer.isEmpty + { + if layerInfo.type.rawValue == "circle", + let isPersistentCircle = try? mapView.mapboxMap.isPersistentLayer(id: layerInfo.id) + { + let pitchAlignment = mapView.mapboxMap.layerProperty( + for: layerInfo.id, + property: "circle-pitch-alignment" + ).value as? String + if isPersistentCircle || (pitchAlignment != "map") { + continue + } + } + targetLayer = layerInfo.id + } + } else if isAboveRoadLayer { + // find the topmost road name label layer for layerIdentifier in arrowLayers. + if targetLayer == nil, + layerInfo.id.contains("road-label"), + mapView.mapboxMap.layerExists(withId: layerInfo.id) + { + targetLayer = layerInfo.id + } + } else { + // find the topmost layer for layerIdentifier in uppermostSymbolLayers. + if targetLayer == nil, + let sourceLayer = mapView.mapboxMap.layerProperty(for: layerInfo.id, property: "source-layer") + .value as? String, + !sourceLayer.isEmpty + { + targetLayer = layerInfo.id + } + } + } + + guard let targetLayer else { return layerPosition } + guard let layerPosition else { return .above(targetLayer) } + + if isLowermostLayer { + // For layers should be below symbol layers. + if case .below(let sequenceLayer) = layerPosition, !lowermostSymbolLayers.contains(sequenceLayer) { + // If the sequenceLayer isn't in lowermostSymbolLayers, it's above symbol layer. + // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost non symbol + // layer, + // but under the symbol layers. + return .above(targetLayer) + } + } else if isAboveRoadLayer { + // For layers should be above road name labels but below other symbol layers. + if case .below(let sequenceLayer) = layerPosition, uppermostSymbolLayers.contains(sequenceLayer) { + // If the sequenceLayer is in uppermostSymbolLayers, it's above all symbol layers. + // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost road name + // symbol layer. + return .above(targetLayer) + } else if case .above(let sequenceLayer) = layerPosition, lowermostSymbolLayers.contains(sequenceLayer) { + // If the sequenceLayer is in lowermostSymbolLayers, it's below all symbol layers. + // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost road name + // symbol layer. + return .above(targetLayer) + } + } else { + // For other layers should be uppermost and above symbol layers. + if case .above(let sequenceLayer) = layerPosition, !uppermostSymbolLayers.contains(sequenceLayer) { + // If the sequenceLayer isn't in uppermostSymbolLayers, it's below some symbol layers. + // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost layer. + return .above(targetLayer) + } + } + + return layerPosition + } +} + +extension NavigationMapStyleManager { + // TODO: These ids are specific to Standard style, we should allow customers to customize this + var poiLayerIds: [String] { + let poiLayerIds = layerIds.filter { layerId in + NavigationMapView.LayerIdentifier.clickablePoiLabels.contains { + layerId.hasPrefix($0) + } + } + return Array(poiLayerIds) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift new file mode 100644 index 000000000..93f376473 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift @@ -0,0 +1,364 @@ +import _MapboxNavigationHelpers +import MapboxDirections +import MapboxMaps +import MapboxNavigationNative +import enum SwiftUI.ColorScheme +import UIKit + +extension NavigationRoutes { + func routeAlertsAnnotationsMapFeatures( + ids: FeatureIds.RouteAlertAnnotation, + distanceTraveled: CLLocationDistance, + customizedLayerProvider: CustomizedLayerProvider, + excludedRouteAlertTypes: RoadAlertType + ) -> [MapFeature] { + let convertedRouteAlerts = mainRoute.nativeRoute.getRouteInfo().alerts.map { + RoadObjectAhead( + roadObject: RoadObject($0.roadObject), + distance: $0.distanceToStart + ) + } + + return convertedRouteAlerts.routeAlertsAnnotationsMapFeatures( + ids: ids, + distanceTraveled: distanceTraveled, + customizedLayerProvider: customizedLayerProvider, + excludedRouteAlertTypes: excludedRouteAlertTypes + ) + } +} + +extension [RoadObjectAhead] { + func routeAlertsAnnotationsMapFeatures( + ids: FeatureIds.RouteAlertAnnotation, + distanceTraveled: CLLocationDistance, + customizedLayerProvider: CustomizedLayerProvider, + excludedRouteAlertTypes: RoadAlertType + ) -> [MapFeature] { + let featureCollection = FeatureCollection(features: roadObjectsFeatures( + for: self, + currentDistance: distanceTraveled, + excludedRouteAlertTypes: excludedRouteAlertTypes + )) + let layers: [any Layer] = [ + with(SymbolLayer(id: ids.layer, source: ids.source)) { + $0.iconImage = .expression(Exp(.get) { RoadObjectInfo.objectImageType }) + $0.minZoom = 10 + + $0.iconSize = .expression( + Exp(.interpolate) { + Exp(.linear) + Exp(.zoom) + Self.interpolationFactors.mapValues { $0 * 0.2 } + } + ) + + $0.iconColor = .expression(Exp(.get) { RoadObjectInfo.objectColor }) + }, + ] + return [ + GeoJsonMapFeature( + id: ids.featureId, + sources: [ + .init( + id: ids.source, + geoJson: .featureCollection(featureCollection) + ), + ], + customizeSource: { _, _ in }, + layers: layers.map { customizedLayerProvider.customizedLayer($0) }, + onBeforeAdd: { mapView in + Self.upsertRouteAlertsSymbolImages( + map: mapView.mapboxMap + ) + }, + onUpdate: { mapView in + Self.upsertRouteAlertsSymbolImages( + map: mapView.mapboxMap + ) + }, + onAfterRemove: { mapView in + do { + try Self.removeRouteAlertSymbolImages( + from: mapView.mapboxMap + ) + } catch { + Log.error( + "Failed to remove route alerts annotation images with error \(error)", + category: .navigationUI + ) + } + } + ), + ] + } + + private static let interpolationFactors = [ + 10.0: 1.0, + 14.5: 3.0, + 17.0: 6.0, + 22.0: 8.0, + ] + + private func roadObjectsFeatures( + for alerts: [RoadObjectAhead], + currentDistance: CLLocationDistance, + excludedRouteAlertTypes: RoadAlertType + ) -> [Feature] { + var features = [Feature]() + for alert in alerts where !alert.isExcluded(excludedRouteAlertTypes: excludedRouteAlertTypes) { + guard alert.distance == nil || alert.distance! >= currentDistance, + let objectInfo = info(for: alert.roadObject.kind) + else { continue } + let object = alert.roadObject + func addImage( + _ coordinate: LocationCoordinate2D, + _ distance: LocationDistance?, + color: UIColor? = nil + ) { + var feature = Feature(geometry: .point(.init(coordinate))) + let identifier: FeatureIdentifier = + .string("road-alert-\(coordinate.latitude)-\(coordinate.longitude)-\(features.count)") + let colorHex = (color ?? objectInfo.color ?? UIColor.gray).hexString + let properties: [String: JSONValue?] = [ + RoadObjectInfo.objectColor: JSONValue(rawValue: colorHex ?? UIColor.gray.hexString!), + RoadObjectInfo.objectImageType: .string(objectInfo.imageType.rawValue), + RoadObjectInfo.objectDistanceFromStart: .number(distance ?? 0.0), + RoadObjectInfo.distanceTraveled: .number(0.0), + ] + feature.properties = properties + feature.identifier = identifier + features.append(feature) + } + switch object.location { + case .routeAlert(shape: .lineString(let shape)): + guard + let startCoordinate = shape.coordinates.first, + let endCoordinate = shape.coordinates.last + else { + break + } + + if alert.distance.map({ $0 > 0 }) ?? true { + addImage(startCoordinate, alert.distance, color: .blue) + } + addImage(endCoordinate, alert.distance.map { $0 + (object.length ?? 0) }, color: .red) + case .routeAlert(shape: .point(let point)): + addImage(point.coordinates, alert.distance, color: nil) + case .openLRPoint(position: _, sideOfRoad: _, orientation: _, coordinate: let coordinates): + addImage(coordinates, alert.distance, color: nil) + case .openLRLine(path: _, shape: let geometry): + guard + let shape = openLRShape(from: geometry), + let startCoordinate = shape.coordinates.first, + let endCoordinate = shape.coordinates.last + else { + break + } + if alert.distance.map({ $0 > 0 }) ?? true { + addImage(startCoordinate, alert.distance, color: .blue) + } + addImage(endCoordinate, alert.distance.map { $0 + (object.length ?? 0) }, color: .red) + case .subgraph(enters: let enters, exits: let exits, shape: _, edges: _): + for enter in enters { + addImage(enter.coordinate, nil, color: .blue) + } + for exit in exits { + addImage(exit.coordinate, nil, color: .red) + } + default: + Log.error( + "Unexpected road object as Route Alert: \(object.identifier):\(object.kind)", + category: .navigationUI + ) + } + } + return features + } + + private func openLRShape(from geometry: Geometry) -> LineString? { + switch geometry { + case .point(let point): + return .init([point.coordinates]) + case .lineString(let lineString): + return lineString + default: + break + } + return nil + } + + private func info(for objectKind: RoadObject.Kind) -> RoadObjectInfo? { + switch objectKind { + case .incident(let incident): + let text = incident?.description + let color = incident?.impact.map(color(for:)) + switch incident?.kind { + case .congestion: + return .init(.congestion, text: text, color: color) + case .construction: + return .init(.construction, text: text, color: color) + case .roadClosure: + return .init(.roadClosure, text: text, color: color) + case .accident: + return .init(.accident, text: text, color: color) + case .disabledVehicle: + return .init(.disabledVehicle, text: text, color: color) + case .laneRestriction: + return .init(.laneRestriction, text: text, color: color) + case .massTransit: + return .init(.massTransit, text: text, color: color) + case .miscellaneous: + return .init(.miscellaneous, text: text, color: color) + case .otherNews: + return .init(.otherNews, text: text, color: color) + case .plannedEvent: + return .init(.plannedEvent, text: text, color: color) + case .roadHazard: + return .init(.roadHazard, text: text, color: color) + case .weather: + return .init(.weather, text: text, color: color) + case .undefined, .none: + return nil + } + default: + // We only show incidents on the map + return nil + } + } + + private func color(for impact: Incident.Impact) -> UIColor { + switch impact { + case .critical: + return .red + case .major: + return .purple + case .minor: + return .orange + case .low: + return .blue + case .unknown: + return .gray + } + } + + private static func upsertRouteAlertsSymbolImages( + map: MapboxMap + ) { + for (imageName, imageIdentifier) in imageNameToMapIdentifier(ids: RoadObjectFeature.ImageType.allCases) { + if let image = Bundle.module.image(named: imageName) { + map.provisionImage(id: imageIdentifier) { _ in + try map.addImage(image, id: imageIdentifier) + } + } else { + assertionFailure("No image for route alert \(imageName) in the bundle.") + } + } + } + + private static func removeRouteAlertSymbolImages( + from map: MapboxMap + ) throws { + for (_, imageIdentifier) in imageNameToMapIdentifier(ids: RoadObjectFeature.ImageType.allCases) { + try map.removeImage(withId: imageIdentifier) + } + } + + private static func imageNameToMapIdentifier( + ids: [RoadObjectFeature.ImageType] + ) -> [String: String] { + return ids.reduce(into: [String: String]()) { partialResult, type in + partialResult[type.imageName] = type.rawValue + } + } + + private struct RoadObjectFeature: Equatable { + enum ImageType: String, CaseIterable { + case accident + case congestion + case construction + case disabledVehicle = "disabled_vehicle" + case laneRestriction = "lane_restriction" + case massTransit = "mass_transit" + case miscellaneous + case otherNews = "other_news" + case plannedEvent = "planned_event" + case roadClosure = "road_closure" + case roadHazard = "road_hazard" + case weather + + var imageName: String { + switch self { + case .accident: + return "ra_accident" + case .congestion: + return "ra_congestion" + case .construction: + return "ra_construction" + case .disabledVehicle: + return "ra_disabled_vehicle" + case .laneRestriction: + return "ra_lane_restriction" + case .massTransit: + return "ra_mass_transit" + case .miscellaneous: + return "ra_miscellaneous" + case .otherNews: + return "ra_other_news" + case .plannedEvent: + return "ra_planned_event" + case .roadClosure: + return "ra_road_closure" + case .roadHazard: + return "ra_road_hazard" + case .weather: + return "ra_weather" + } + } + } + + struct Image: Equatable { + var id: String? + var type: ImageType + var coordinate: LocationCoordinate2D + var color: UIColor? + var text: String? + var isOnMainRoute: Bool + } + + struct Shape: Equatable { + var geometry: Geometry + } + + var id: String + var images: [Image] + var shape: Shape? + } + + private struct RoadObjectInfo { + var imageType: RoadObjectFeature.ImageType + var text: String? + var color: UIColor? + + init(_ imageType: RoadObjectFeature.ImageType, text: String? = nil, color: UIColor? = nil) { + self.imageType = imageType + self.text = text + self.color = color + } + + static let objectColor = "objectColor" + static let objectImageType = "objectImageType" + static let objectDistanceFromStart = "objectDistanceFromStart" + static let distanceTraveled = "distanceTraveled" + } +} + +extension RoadObjectAhead { + fileprivate func isExcluded(excludedRouteAlertTypes: RoadAlertType) -> Bool { + guard let roadAlertType = RoadAlertType(roadObjectKind: roadObject.kind) else { + return false + } + + return excludedRouteAlertTypes.contains(roadAlertType) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift new file mode 100644 index 000000000..90c7a55bd --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift @@ -0,0 +1,55 @@ +import _MapboxNavigationHelpers +import CoreLocation +import MapboxDirections +import MapboxMaps +import Turf +import UIKit + +/// Describes the possible annotation types on the route line. +public enum RouteAnnotationKind { + /// Shows the route duration. + case routeDurations + /// Shows the relative diff between the main route and the alternative. + /// The annotation is displayed in the approximate middle of the alternative steps. + case relativeDurationsOnAlternative + /// Shows the relative diff between the main route and the alternative. + /// The annotation is displayed next to the first different maneuver of the alternative road. + case relativeDurationsOnAlternativeManuever +} + +extension NavigationRoutes { + func routeDurationMapFeatures( + annotationKinds: Set, + config: MapStyleConfig + ) -> [any MapFeature] { + var showMainRoute = false + var showAlternatives = false + var showAsRelative = false + var annotateManeuver = false + for annotationKind in annotationKinds { + switch annotationKind { + case .routeDurations: + showMainRoute = true + showAlternatives = config.showsAlternatives + case .relativeDurationsOnAlternative: + showAsRelative = true + showAlternatives = config.showsAlternatives + case .relativeDurationsOnAlternativeManuever: + showAsRelative = true + annotateManeuver = true + showAlternatives = config.showsAlternatives + } + } + + return [ + ETAViewsAnnotationFeature( + for: self, + showMainRoute: showMainRoute, + showAlternatives: showAlternatives, + isRelative: showAsRelative, + annotateAtManeuver: annotateManeuver, + mapStyleConfig: config + ), + ] + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift new file mode 100644 index 000000000..66215f6f4 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift @@ -0,0 +1,406 @@ +import _MapboxNavigationHelpers +import MapboxDirections +@_spi(Experimental) import MapboxMaps +import Turf +import UIKit + +struct LineGradientSettings { + let isSoft: Bool + let baseColor: UIColor + let featureColor: (Turf.Feature) -> UIColor +} + +struct RouteLineFeatureProvider { + var customRouteLineLayer: (String, String) -> Layer? + var customRouteCasingLineLayer: (String, String) -> Layer? + var customRouteRestrictedAreasLineLayer: (String, String) -> Layer? +} + +extension Route { + func routeLineMapFeatures( + ids: FeatureIds.RouteLine, + offset: Double, + isSoftGradient: Bool, + isAlternative: Bool, + config: MapStyleConfig, + featureProvider: RouteLineFeatureProvider, + customizedLayerProvider: CustomizedLayerProvider + ) -> [any MapFeature] { + var features: [any MapFeature] = [] + + if let shape { + let congestionFeatures = congestionFeatures( + legIndex: nil, + rangesConfiguration: config.congestionConfiguration.ranges + ) + let gradientStops = routeLineCongestionGradient( + congestionFeatures: congestionFeatures, + isMain: !isAlternative, + isSoft: isSoftGradient, + config: config + ) + let colors = config.congestionConfiguration.colors + let trafficGradient: Value = .expression( + .routeLineGradientExpression( + gradientStops, + lineBaseColor: isAlternative ? colors.alternativeRouteColors.unknown : colors.mainRouteColors + .unknown, + isSoft: isSoftGradient + ) + ) + + var sources: [GeoJsonMapFeature.Source] = [ + .init( + id: ids.source, + geoJson: .init(Feature(geometry: .lineString(shape))) + ), + ] + + let customRouteLineLayer = featureProvider.customRouteLineLayer(ids.main, ids.source) + let customRouteCasingLineLayer = featureProvider.customRouteCasingLineLayer(ids.casing, ids.source) + var layers: [any Layer] = [ + customRouteLineLayer ?? customizedLayerProvider.customizedLayer(defaultRouteLineLayer( + ids: ids, + isAlternative: isAlternative, + trafficGradient: trafficGradient, + config: config + )), + customRouteCasingLineLayer ?? customizedLayerProvider.customizedLayer(defaultRouteCasingLineLayer( + ids: ids, + isAlternative: isAlternative, + config: config + )), + ] + + if let traversedRouteColor = config.traversedRouteColor, !isAlternative, config.routeLineTracksTraversal { + layers.append( + customizedLayerProvider.customizedLayer(defaultTraversedRouteLineLayer( + ids: ids, + traversedRouteColor: traversedRouteColor, + config: config + )) + ) + } + + let restrictedRoadsFeatures: [Feature]? = config.isRestrictedAreaEnabled ? restrictedRoadsFeatures() : nil + let restrictedAreaGradientExpression: Value? = restrictedRoadsFeatures + .map { routeLineRestrictionsGradient($0, config: config) } + .map { + .expression( + MapboxMaps.Expression.routeLineGradientExpression( + $0, + lineBaseColor: config.routeRestrictedAreaColor + ) + ) + } + + if let restrictedRoadsFeatures, let restrictedAreaGradientExpression { + let shape = LineString(restrictedRoadsFeatures.compactMap { + guard case .lineString(let lineString) = $0.geometry else { + return nil + } + return lineString.coordinates + }.reduce([CLLocationCoordinate2D](), +)) + + sources.append( + .init( + id: ids.restrictedAreaSource, + geoJson: .geometry(.lineString(shape)) + ) + ) + let customRouteRestrictedAreasLine = featureProvider.customRouteRestrictedAreasLineLayer( + ids.restrictedArea, + ids.restrictedAreaSource + ) + + layers.append( + customRouteRestrictedAreasLine ?? + customizedLayerProvider.customizedLayer(defaultRouteRestrictedAreasLine( + ids: ids, + gradientExpression: restrictedAreaGradientExpression, + config: config + )) + ) + } + + features.append( + GeoJsonMapFeature( + id: ids.main, + sources: sources, + customizeSource: { source, _ in + source.lineMetrics = true + source.tolerance = 0.375 + }, + layers: layers, + onAfterAdd: { mapView in + mapView.mapboxMap.setRouteLineOffset(offset, for: ids) + }, + onUpdate: { mapView in + mapView.mapboxMap.setRouteLineOffset(offset, for: ids) + }, + onAfterUpdate: { mapView in + let map: MapboxMap = mapView.mapboxMap + try map.updateLayer(withId: ids.main, type: LineLayer.self, update: { layer in + layer.lineGradient = trafficGradient + }) + if let restrictedAreaGradientExpression { + try map.updateLayer(withId: ids.restrictedArea, type: LineLayer.self, update: { layer in + layer.lineGradient = restrictedAreaGradientExpression + }) + } + } + ) + ) + } + + return features + } + + private func defaultRouteLineLayer( + ids: FeatureIds.RouteLine, + isAlternative: Bool, + trafficGradient: Value, + config: MapStyleConfig + ) -> LineLayer { + let colors = config.congestionConfiguration.colors + let routeColors = isAlternative ? colors.alternativeRouteColors : colors.mainRouteColors + return with(LineLayer(id: ids.main, source: ids.source)) { + $0.lineColor = .constant(.init(routeColors.unknown)) + $0.lineWidth = .expression(.routeLineWidthExpression()) + $0.lineJoin = .constant(.round) + $0.lineCap = .constant(.round) + $0.lineGradient = trafficGradient + $0.lineDepthOcclusionFactor = config.occlusionFactor + $0.lineEmissiveStrength = .constant(1) + } + } + + private func defaultRouteCasingLineLayer( + ids: FeatureIds.RouteLine, + isAlternative: Bool, + config: MapStyleConfig + ) -> LineLayer { + let lineColor = isAlternative ? config.routeAlternateCasingColor : config.routeCasingColor + return with(LineLayer(id: ids.casing, source: ids.source)) { + $0.lineColor = .constant(.init(lineColor)) + $0.lineWidth = .expression(.routeCasingLineWidthExpression()) + $0.lineJoin = .constant(.round) + $0.lineCap = .constant(.round) + $0.lineDepthOcclusionFactor = config.occlusionFactor + $0.lineEmissiveStrength = .constant(1) + } + } + + private func defaultTraversedRouteLineLayer( + ids: FeatureIds.RouteLine, + traversedRouteColor: UIColor, + config: MapStyleConfig + ) -> LineLayer { + return with(LineLayer(id: ids.traversedRoute, source: ids.source)) { + $0.lineColor = .constant(.init(traversedRouteColor)) + $0.lineWidth = .expression(.routeLineWidthExpression()) + $0.lineJoin = .constant(.round) + $0.lineCap = .constant(.round) + $0.lineDepthOcclusionFactor = config.occlusionFactor + $0.lineEmissiveStrength = .constant(1) + } + } + + private func defaultRouteRestrictedAreasLine( + ids: FeatureIds.RouteLine, + gradientExpression: Value?, + config: MapStyleConfig + ) -> LineLayer { + return with(LineLayer(id: ids.restrictedArea, source: ids.restrictedAreaSource)) { + $0.lineColor = .constant(.init(config.routeRestrictedAreaColor)) + $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.5)) + $0.lineJoin = .constant(.round) + $0.lineCap = .constant(.round) + $0.lineOpacity = .constant(0.5) + $0.lineDepthOcclusionFactor = config.occlusionFactor + + $0.lineGradient = gradientExpression + $0.lineDasharray = .constant([0.5, 2.0]) + } + } + + func routeLineCongestionGradient( + congestionFeatures: [Turf.Feature]? = nil, + isMain: Bool = true, + isSoft: Bool, + config: MapStyleConfig + ) -> [Double: UIColor] { + // If `congestionFeatures` is set to nil - check if overridden route line casing is used. + let colors = config.congestionConfiguration.colors + let baseColor: UIColor = if let _ = congestionFeatures { + isMain ? colors.mainRouteColors.unknown : colors.alternativeRouteColors.unknown + } else { + config.routeCasingColor + } + let configuration = config.congestionConfiguration.colors + + let lineSettings = LineGradientSettings( + isSoft: isSoft, + baseColor: baseColor, + featureColor: { + guard config.showsTrafficOnRouteLine else { + return baseColor + } + if case .boolean(let isCurrentLeg) = $0.properties?[CurrentLegAttribute], isCurrentLeg { + let colors = isMain ? configuration.mainRouteColors : configuration.alternativeRouteColors + if case .string(let congestionLevel) = $0.properties?[CongestionAttribute] { + return congestionColor(for: congestionLevel, with: colors) + } else { + return congestionColor(for: nil, with: colors) + } + } + + return config.routeCasingColor + } + ) + + return routeLineFeaturesGradient(congestionFeatures, lineSettings: lineSettings) + } + + /// Given a congestion level, return its associated color. + func congestionColor(for congestionLevel: String?, with colors: CongestionColorsConfiguration.Colors) -> UIColor { + switch congestionLevel { + case "low": + return colors.low + case "moderate": + return colors.moderate + case "heavy": + return colors.heavy + case "severe": + return colors.severe + default: + return colors.unknown + } + } + + func routeLineFeaturesGradient( + _ routeLineFeatures: [Turf.Feature]? = nil, + lineSettings: LineGradientSettings + ) -> [Double: UIColor] { + var gradientStops = [Double: UIColor]() + var distanceTraveled = 0.0 + + if let routeLineFeatures { + let routeDistance = routeLineFeatures.compactMap { feature -> LocationDistance? in + if case .lineString(let lineString) = feature.geometry { + return lineString.distance() + } else { + return nil + } + }.reduce(0, +) + // lastRecordSegment records the last segmentEndPercentTraveled and associated congestion color added to the + // gradientStops. + var lastRecordSegment: (Double, UIColor) = (0.0, .clear) + + for (index, feature) in routeLineFeatures.enumerated() { + let associatedFeatureColor = lineSettings.featureColor(feature) + + guard case .lineString(let lineString) = feature.geometry, + let distance = lineString.distance() + else { + if gradientStops.isEmpty { + gradientStops[0.0] = lineSettings.baseColor + } + return gradientStops + } + let minimumPercentGap = 2e-16 + let stopGap = (routeDistance > 0.0) ? max( + min(GradientCongestionFadingDistance, distance * 0.1) / routeDistance, + minimumPercentGap + ) : minimumPercentGap + + if index == routeLineFeatures.startIndex { + distanceTraveled = distanceTraveled + distance + gradientStops[0.0] = associatedFeatureColor + + if index + 1 < routeLineFeatures.count { + let segmentEndPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 + var currentGradientStop = lineSettings + .isSoft ? segmentEndPercentTraveled - stopGap : + Double(CGFloat(segmentEndPercentTraveled).nextDown) + currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) + gradientStops[currentGradientStop] = associatedFeatureColor + lastRecordSegment = (currentGradientStop, associatedFeatureColor) + } + + continue + } + + if index == routeLineFeatures.endIndex - 1 { + if associatedFeatureColor == lastRecordSegment.1 { + gradientStops[lastRecordSegment.0] = nil + } else { + let segmentStartPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 + var currentGradientStop = lineSettings + .isSoft ? segmentStartPercentTraveled + stopGap : + Double(CGFloat(segmentStartPercentTraveled).nextUp) + currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) + gradientStops[currentGradientStop] = associatedFeatureColor + } + + continue + } + + if associatedFeatureColor == lastRecordSegment.1 { + gradientStops[lastRecordSegment.0] = nil + } else { + let segmentStartPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 + var currentGradientStop = lineSettings + .isSoft ? segmentStartPercentTraveled + stopGap : + Double(CGFloat(segmentStartPercentTraveled).nextUp) + currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) + gradientStops[currentGradientStop] = associatedFeatureColor + } + + distanceTraveled = distanceTraveled + distance + let segmentEndPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 + var currentGradientStop = lineSettings + .isSoft ? segmentEndPercentTraveled - stopGap : Double(CGFloat(segmentEndPercentTraveled).nextDown) + currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) + gradientStops[currentGradientStop] = associatedFeatureColor + lastRecordSegment = (currentGradientStop, associatedFeatureColor) + } + + if gradientStops.isEmpty { + gradientStops[0.0] = lineSettings.baseColor + } + + } else { + gradientStops[0.0] = lineSettings.baseColor + } + + return gradientStops + } + + func routeLineRestrictionsGradient( + _ restrictionFeatures: [Turf.Feature], + config: MapStyleConfig + ) -> [Double: UIColor] { + // If there's no restricted feature, hide the restricted route line layer. + guard restrictionFeatures.count > 0 else { + let gradientStops: [Double: UIColor] = [0.0: .clear] + return gradientStops + } + + let lineSettings = LineGradientSettings( + isSoft: false, + baseColor: config.routeRestrictedAreaColor, + featureColor: { + if case .boolean(let isRestricted) = $0.properties?[RestrictedRoadClassAttribute], + isRestricted + { + return config.routeRestrictedAreaColor + } + + return .clear // forcing hiding non-restricted areas + } + ) + + return routeLineFeaturesGradient(restrictionFeatures, lineSettings: lineSettings) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift new file mode 100644 index 000000000..42f86f6a3 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift @@ -0,0 +1,66 @@ +import _MapboxNavigationHelpers +import MapboxDirections +import MapboxMaps +import Turf + +extension Route { + func voiceInstructionMapFeatures( + ids: FeatureIds.VoiceInstruction, + customizedLayerProvider: CustomizedLayerProvider + ) -> [any MapFeature] { + var featureCollection = FeatureCollection(features: []) + + for (legIndex, leg) in legs.enumerated() { + for (stepIndex, step) in leg.steps.enumerated() { + guard let instructions = step.instructionsSpokenAlongStep else { continue } + for instruction in instructions { + guard let shape = legs[legIndex].steps[stepIndex].shape, + let coordinateFromStart = LineString(shape.coordinates.reversed()) + .coordinateFromStart(distance: instruction.distanceAlongStep) else { continue } + + var feature = Feature(geometry: .point(Point(coordinateFromStart))) + feature.properties = [ + "instruction": .string(instruction.text), + ] + featureCollection.features.append(feature) + } + } + } + + let layers: [any Layer] = [ + with(SymbolLayer(id: ids.layer, source: ids.source)) { + let instruction = Exp(.toString) { + Exp(.get) { + "instruction" + } + } + + $0.textField = .expression(instruction) + $0.textSize = .constant(14) + $0.textHaloWidth = .constant(1) + $0.textHaloColor = .constant(.init(.white)) + $0.textOpacity = .constant(0.75) + $0.textAnchor = .constant(.bottom) + $0.textJustify = .constant(.left) + }, + with(CircleLayer(id: ids.circleLayer, source: ids.source)) { + $0.circleRadius = .constant(5) + $0.circleOpacity = .constant(0.75) + $0.circleColor = .constant(.init(.white)) + }, + ] + return [ + GeoJsonMapFeature( + id: ids.source, + sources: [ + .init( + id: ids.source, + geoJson: .featureCollection(featureCollection) + ), + ], + customizeSource: { _, _ in }, + layers: layers.map { customizedLayerProvider.customizedLayer($0) } + ), + ] + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift new file mode 100644 index 000000000..202043cf2 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift @@ -0,0 +1,156 @@ +import _MapboxNavigationHelpers +import MapboxDirections +import MapboxMaps +import Turf +import UIKit + +struct WaypointFeatureProvider { + var customFeatures: ([Waypoint], Int) -> FeatureCollection? + var customCirleLayer: (String, String) -> CircleLayer? + var customSymbolLayer: (String, String) -> SymbolLayer? +} + +@MainActor +extension Route { + /// Generates a map feature that visually represents waypoints along a route line. + /// The waypoints include the start, destination, and any intermediate waypoints. + /// - Important: Only intermediate waypoints are marked with pins. The starting point and destination are excluded + /// from this. + func waypointsMapFeature( + mapView: MapView, + legIndex: Int, + config: MapStyleConfig, + featureProvider: WaypointFeatureProvider, + customizedLayerProvider: CustomizedLayerProvider + ) -> MapFeature? { + guard let startWaypoint = legs.first?.source else { return nil } + guard let destinationWaypoint = legs.last?.destination else { return nil } + + let intermediateWaypoints = config.showsIntermediateWaypoints + ? legs.dropLast().compactMap(\.destination) + : [] + let waypoints = [startWaypoint] + intermediateWaypoints + [destinationWaypoint] + + registerIntermediateWaypointImage(in: mapView) + + let customFeatures = featureProvider.customFeatures(waypoints, legIndex) + + return waypointsMapFeature( + with: customFeatures ?? waypointsFeatures(legIndex: legIndex, waypoints: waypoints), + config: config, + featureProvider: featureProvider, + customizedLayerProvider: customizedLayerProvider + ) + } + + private func waypointsFeatures(legIndex: Int, waypoints: [Waypoint]) -> FeatureCollection { + FeatureCollection( + features: waypoints.enumerated().map { waypointIndex, waypoint in + var feature = Feature(geometry: .point(Point(waypoint.coordinate))) + var properties: [String: JSONValue] = [:] + properties["waypointCompleted"] = .boolean(waypointIndex <= legIndex) + properties["waipointIconImage"] = waypointIndex > 0 && waypointIndex < waypoints.count - 1 + ? .string(NavigationMapView.ImageIdentifier.midpointMarkerImage) + : nil + feature.properties = properties + + return feature + } + ) + } + + private func registerIntermediateWaypointImage(in mapView: MapView) { + let intermediateWaypointImageId = NavigationMapView.ImageIdentifier.midpointMarkerImage + mapView.mapboxMap.provisionImage(id: intermediateWaypointImageId) { + try $0.addImage( + UIImage.midpointMarkerImage, + id: intermediateWaypointImageId, + stretchX: [], + stretchY: [] + ) + } + } + + private func waypointsMapFeature( + with features: FeatureCollection, + config: MapStyleConfig, + featureProvider: WaypointFeatureProvider, + customizedLayerProvider: CustomizedLayerProvider + ) -> MapFeature { + let circleLayer = featureProvider.customCirleLayer( + FeatureIds.RouteWaypoints.default.innerCircle, + FeatureIds.RouteWaypoints.default.source + ) ?? customizedLayerProvider.customizedLayer(defaultCircleLayer(config: config)) + + let symbolLayer = featureProvider.customSymbolLayer( + FeatureIds.RouteWaypoints.default.markerIcon, + FeatureIds.RouteWaypoints.default.source + ) ?? customizedLayerProvider.customizedLayer(defaultSymbolLayer) + + return GeoJsonMapFeature( + id: FeatureIds.RouteWaypoints.default.featureId, + sources: [ + .init( + id: FeatureIds.RouteWaypoints.default.source, + geoJson: .featureCollection(features) + ), + ], + customizeSource: { _, _ in }, + layers: [circleLayer, symbolLayer], + onBeforeAdd: { _ in }, + onAfterRemove: { _ in } + ) + } + + private func defaultCircleLayer(config: MapStyleConfig) -> CircleLayer { + with( + CircleLayer( + id: FeatureIds.RouteWaypoints.default.innerCircle, + source: FeatureIds.RouteWaypoints.default.source + ) + ) { + let opacity = Exp(.switchCase) { + Exp(.any) { + Exp(.get) { + "waypointCompleted" + } + } + 0 + 1 + } + + $0.circleColor = .constant(.init(config.waypointColor)) + $0.circleOpacity = .expression(opacity) + $0.circleEmissiveStrength = .constant(1) + $0.circleRadius = .expression(.routeCasingLineWidthExpression(0.5)) + $0.circleStrokeColor = .constant(.init(config.waypointStrokeColor)) + $0.circleStrokeWidth = .expression(.routeCasingLineWidthExpression(0.14)) + $0.circleStrokeOpacity = .expression(opacity) + $0.circlePitchAlignment = .constant(.map) + } + } + + private var defaultSymbolLayer: SymbolLayer { + with( + SymbolLayer( + id: FeatureIds.RouteWaypoints.default.markerIcon, + source: FeatureIds.RouteWaypoints.default.source + ) + ) { + let opacity = Exp(.switchCase) { + Exp(.any) { + Exp(.get) { + "waypointCompleted" + } + } + 0 + 1 + } + $0.iconOpacity = .expression(opacity) + $0.iconImage = .expression(Exp(.get) { "waipointIconImage" }) + $0.iconAnchor = .constant(.bottom) + $0.iconOffset = .constant([0, 15]) + $0.iconAllowOverlap = .constant(true) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/ElectronicHorizonController.swift b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/ElectronicHorizonController.swift new file mode 100644 index 000000000..c2af65093 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/ElectronicHorizonController.swift @@ -0,0 +1,20 @@ +import Combine +import Foundation + +/// Provides access to ElectronicHorizon events. +@MainActor +public protocol ElectronicHorizonController: Sendable { + /// Posts updates on EH. + var eHorizonEvents: AnyPublisher { get } + /// Provides access to the road graph network and related road objects. + var roadMatching: RoadMatching { get } + + /// Toggles ON EH updates. + /// + /// Requires ``ElectronicHorizonConfig`` to be provided. + func startUpdatingEHorizon() + /// Toggles OFF EH updates. + /// + /// Requires ``ElectronicHorizonConfig`` to be provided. + func stopUpdatingEHorizon() +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/MapboxNavigation.swift b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/MapboxNavigation.swift new file mode 100644 index 000000000..6f3a08f39 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/MapboxNavigation.swift @@ -0,0 +1,47 @@ +import Combine +import Foundation + +/// An entry point for interacting with the Mapbox Navigation SDK. +@MainActor +public protocol MapboxNavigation { + /// Returns a ``RoutingProvider`` used by SDK + func routingProvider() -> RoutingProvider + + /// Provides control over main navigation states and transitions between them. + func tripSession() -> SessionController + + // TODO: add replaying controls + + /// Provides access to ElectronicHorizon events. + func electronicHorizon() -> ElectronicHorizonController + + /// Provides control over various aspects of the navigation process, mainly Active Guidance. + func navigation() -> NavigationController + + /// Provides access to observing and posting various navigation events and user feedback. + func eventsManager() -> NavigationEventsManager + + /// Provides ability to push custom history events to the log. + func historyRecorder() -> HistoryRecording? + + /// Provides access to the copilot service. + /// + /// Use this to get fine details of the current navigation session and manually control it. + func copilot() -> CopilotService? +} + +extension MapboxNavigator: + SessionController, + ElectronicHorizonController, + NavigationController +{ + public var locationMatching: AnyPublisher { + mapMatching + .compactMap { $0 } + .eraseToAnyPublisher() + } + + var currentLocationMatching: MapMatchingState? { + currentMapMatching + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/NavigationController.swift b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/NavigationController.swift new file mode 100644 index 000000000..466552f3b --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/NavigationController.swift @@ -0,0 +1,60 @@ +import Combine +import CoreLocation +import Foundation +import MapboxDirections + +/// Provides control over various aspects of the navigation process, mainly Active Guidance. +@MainActor +public protocol NavigationController: Sendable { + /// Programmatically switches between available continuous alternatvies + /// - parameter index: An index of an alternative in ``NavigationRoutes/alternativeRoutes`` + func selectAlternativeRoute(at index: Int) + /// Programmatically switches between available continuous alternatvies + /// - parameter routeId: ``AlternativeRoute/id-swift.property`` of an alternative + func selectAlternativeRoute(with routeId: RouteId) + /// Manually switches current route leg. + /// - parameter newLegIndex: A leg index to switch to. + func switchLeg(newLegIndex: Int) + /// Posts heading updates. + var heading: AnyPublisher { get } + + /// Posts map matching updates, including location, current speed and speed limits, road info and map matching + /// details. + /// + /// - Note: To receive map matching updates through subscribres, initiate a free drive session by + /// invoking ``SessionController/startFreeDrive()`` or start an active guidance session + /// by invoking ``SessionController/startActiveGuidance(with:startLegIndex:)``. + var locationMatching: AnyPublisher { get } + /// Includes current location, speed, road info and additional map matching details. + var currentLocationMatching: MapMatchingState? { get } + /// Posts current route progress updates. + /// + /// - Note: This functionality is limited to the active guidance mode. + var routeProgress: AnyPublisher { get } + /// Current route progress updates. + /// + /// - Note: This functionality is limited to the active guidance mode. + var currentRouteProgress: RouteProgressState? { get } + + /// Posts updates about Navigator going to switch it's tiles version. + var offlineFallbacks: AnyPublisher { get } + + /// Posts updates about upcoming voice instructions. + var voiceInstructions: AnyPublisher { get } + /// Posts updates about upcoming visual instructions. + var bannerInstructions: AnyPublisher { get } + + /// Posts updates about arriving to route waypoints. + var waypointsArrival: AnyPublisher { get } + /// Posts updates about rerouting events and progress. + var rerouting: AnyPublisher { get } + /// Posts updates about continuous alternatives changes during the trip. + var continuousAlternatives: AnyPublisher { get } + /// Posts updates about faster routes applied during the trip. + var fasterRoutes: AnyPublisher { get } + /// Posts updates about route refreshing process. + var routeRefreshing: AnyPublisher { get } + + /// Posts updates about navigation-related errors happen. + var errors: AnyPublisher { get } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/SessionController.swift b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/SessionController.swift new file mode 100644 index 000000000..1353393ea --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/SessionController.swift @@ -0,0 +1,44 @@ +import Combine +import Foundation + +/// Provides control over main navigation states and transitions between them. +@MainActor +public protocol SessionController: Sendable { + /// Transitions (or resumes) to the Free Drive mode. + func startFreeDrive() + /// Pauses the Free Drive. + /// + /// Does nothing if not in the Free Drive mode. + func pauseFreeDrive() + /// Transitions to Idle state. + /// + /// No navigation actions are performed in this state. Location updates are no collected and not processed. + func setToIdle() + /// Starts ActiveNavigation with the given `navigationRoutes`. + /// - parameter navigationRoutes: A route to navigate. + /// - parameter startLegIndex: A leg index, to start with. Usually start from `0`. + func startActiveGuidance(with navigationRoutes: NavigationRoutes, startLegIndex: Int) + + /// Posts updates of the current session state. + var session: AnyPublisher { get } + /// The current session state. + @MainActor + var currentSession: Session { get } + + /// Posts updates about the ``NavigationRoutes`` which navigator follows. + var navigationRoutes: AnyPublisher { get } + /// Current `NavigationRoutes` the navigator is following + var currentNavigationRoutes: NavigationRoutes? { get } + + /// Explicitly attempts to stop location updates on the background. + /// + /// Call this method when app is going background mode and you want to stop user tracking. + /// Works only for Free Drive mode, when ``CoreConfig/disableBackgroundTrackingLocation`` configuration is enabled. + func disableTrackingBackgroundLocationIfNeeded() + + /// Resumes location tracking after restoring from background mode. + /// + /// Call this method on restoring to foreground if you want to continue user tracking. + /// Works only for Free Drive mode, when ``CoreConfig/disableBackgroundTrackingLocation` configuration is enabled. + func restoreTrackingLocationIfNeeded() +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigationProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigationProvider.swift new file mode 100644 index 000000000..bc3fdb0ac --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigationProvider.swift @@ -0,0 +1,374 @@ +import _MapboxNavigationHelpers +import Foundation +import MapboxCommon +import MapboxCommon_Private +import MapboxNavigationNative +import MapboxNavigationNative_Private + +public final class MapboxNavigationProvider { + let multiplexLocationClient: MultiplexLocationClient + + public var skuTokenProvider: SkuTokenProvider { + billingHandler.skuTokenProvider() + } + + public var predictiveCacheManager: PredictiveCacheManager? { + coreConfig.predictiveCacheConfig.map { + PredictiveCacheManager( + predictiveCacheOptions: $0, + tileStore: coreConfig.tilestoreConfig.navigatorLocation.tileStore + ) + } + } + + private var _sharedRouteVoiceController: RouteVoiceController? + @MainActor + public var routeVoiceController: RouteVoiceController { + if let _sharedRouteVoiceController { + return _sharedRouteVoiceController + } else { + let routeVoiceController = RouteVoiceController( + routeProgressing: navigation().routeProgress, + rerouteStarted: navigation().rerouting + .filter { $0.event is ReroutingStatus.Events.FetchingRoute } + .map { _ in } + .eraseToAnyPublisher(), + fasterRouteSet: navigation().fasterRoutes + .filter { $0.event is FasterRoutesStatus.Events.Applied } + .map { _ in } + .eraseToAnyPublisher(), + speechSynthesizer: coreConfig.ttsConfig.speechSynthesizer( + with: coreConfig.locale, + apiConfiguration: coreConfig.credentials.speech, + skuTokenProvider: skuTokenProvider + ) + ) + _sharedRouteVoiceController = routeVoiceController + return routeVoiceController + } + } + + private let _coreConfig: UnfairLocked + + public var coreConfig: CoreConfig { + _coreConfig.read() + } + + /// Creates a new ``MapboxNavigationProvider``. + /// + /// You should never instantiate multiple instances of ``MapboxNavigationProvider`` simultaneously. + /// - parameter coreConfig: A configuration for the SDK. It is recommended not modify the configuration during + /// operation, but it is still possible via ``MapboxNavigationProvider/apply(coreConfig:)``. + public init(coreConfig: CoreConfig) { + Self.checkInstanceIsUnique() + self._coreConfig = .init(coreConfig) + self.multiplexLocationClient = MultiplexLocationClient(source: coreConfig.locationSource) + apply(coreConfig: coreConfig) + SdkInfoRegistryFactory.getInstance().registerSdkInformation(forInfo: SdkInfo.navigationCore.native) + MovementMonitorFactory.setUserDefinedForCustom(movementMonitor) + } + + /// Updates the SDK configuration. + /// + /// It is not recommended to do so due to some updates may be propagated incorrectly. + /// - Parameter coreConfig: The configuration for the SDK. + public func apply(coreConfig: CoreConfig) { + _coreConfig.update(coreConfig) + + let logLevel = NSNumber(value: coreConfig.logLevel.rawValue) + LogConfiguration.setLoggingLevelForCategory("nav-native", upTo: logLevel) + LogConfiguration.setLoggingLevelForUpTo(logLevel) + eventsMetadataProvider.userInfo = coreConfig.telemetryAppMetadata?.configuration + + MapboxOptions.accessToken = coreConfig.credentials.map.accessToken + let copilotEnabled = coreConfig.copilotEnabled + let locationSource = coreConfig.locationSource + let locationClient = multiplexLocationClient + let ttsConfig = coreConfig.ttsConfig + let locale = coreConfig.locale + let speechApiConfiguration = coreConfig.credentials.speech + let skuTokenProvider = skuTokenProvider + + nativeHandlersFactory.locale = coreConfig.locale + Task { @MainActor [_copilot, _sharedRouteVoiceController] in + await _copilot?.setActive(copilotEnabled) + if locationClient.isInitialized { + locationClient.setLocationSource(locationSource) + } + _sharedRouteVoiceController?.speechSynthesizer = ttsConfig.speechSynthesizer( + with: locale, + apiConfiguration: speechApiConfiguration, + skuTokenProvider: skuTokenProvider + ) + } + } + + /// Provides an entry point for interacting with the Mapbox Navigation SDK. + /// + /// This instance is shared. + @MainActor + public var mapboxNavigation: MapboxNavigation { + self + } + + /// Gets TilesetDescriptor that corresponds to the latest available version of routing tiles. + /// + /// It is intended to be used when creating off-line tile packs. + public func getLatestNavigationTilesetDescriptor() -> TilesetDescriptor { + TilesetDescriptorFactory.getLatestForCache(nativeHandlersFactory.cacheHandle) + } + + // MARK: - Instance Lifecycle control + + private static let hasInstance: NSLocked = .init(false) + + private static func checkInstanceIsUnique() { + hasInstance.mutate { hasInstance in + if hasInstance { + Log.fault( + "[BUG] Two simultaneous active navigation cores. Profile the app and make sure that MapboxNavigationProvider is allocated only once.", + category: .navigation + ) + preconditionFailure("MapboxNavigationProvider was instantiated twice.") + } + hasInstance = true + } + } + + private func unregisterUniqueInstance() { + Self.hasInstance.update(false) + } + + deinit { + unregisterUniqueInstance() + } + + // MARK: - Internal members + + private weak var _sharedNavigator: MapboxNavigator? + @MainActor + func navigator() -> MapboxNavigator { + if let sharedNavigator = _sharedNavigator { + return sharedNavigator + } else { + let coreNavigator: CoreNavigator = NativeNavigator( + with: .init( + credentials: coreConfig.credentials.navigation, + nativeHandlersFactory: nativeHandlersFactory, + routingConfig: coreConfig.routingConfig, + predictiveCacheManager: predictiveCacheManager + ) + ) + let fasterRouteController = coreConfig.routingConfig.fasterRouteDetectionConfig.map { + return $0.customFasterRouteProvider ?? FasterRouteController( + configuration: .init( + settings: $0, + initialManeuverAvoidanceRadius: coreConfig.routingConfig.initialManeuverAvoidanceRadius, + routingProvider: routingProvider() + ) + ) + } + + let newNavigator = MapboxNavigator( + configuration: .init( + navigator: coreNavigator, + routeParserType: RouteParser.self, + locationClient: multiplexLocationClient.locationClient, + alternativesAcceptionPolicy: coreConfig.routingConfig.alternativeRoutesDetectionConfig? + .acceptionPolicy, + billingHandler: billingHandler, + multilegAdvancing: coreConfig.multilegAdvancing, + prefersOnlineRoute: coreConfig.routingConfig.prefersOnlineRoute, + disableBackgroundTrackingLocation: coreConfig.disableBackgroundTrackingLocation, + fasterRouteController: fasterRouteController, + electronicHorizonConfig: coreConfig.electronicHorizonConfig, + congestionConfig: coreConfig.congestionConfig, + movementMonitor: movementMonitor + ) + ) + _sharedNavigator = newNavigator + _ = eventsManager() + + multiplexLocationClient.subscribeToNavigatorUpdates( + newNavigator, + source: coreConfig.locationSource + ) + + // Telemetry needs to be created for Navigator + + return newNavigator + } + } + + private var _billingHandler: UnfairLocked = .init(nil) + var billingHandler: BillingHandler { + _billingHandler.mutate { lazyInstance in + if let lazyInstance { + return lazyInstance + } else { + let newInstance = coreConfig.__customBillingHandler?() + ?? BillingHandler.createInstance(with: coreConfig.credentials.navigation.accessToken) + lazyInstance = newInstance + return newInstance + } + } + } + + lazy var nativeHandlersFactory: NativeHandlersFactory = .init( + tileStorePath: coreConfig.tilestoreConfig.navigatorLocation.tileStoreURL?.path ?? "", + apiConfiguration: coreConfig.credentials.navigation, + tilesVersion: coreConfig.tilesVersion, + targetVersion: nil, + configFactoryType: ConfigFactory.self, + datasetProfileIdentifier: coreConfig.routeRequestConfig.profileIdentifier, + routingProviderSource: coreConfig.routingConfig.routingProviderSource.nativeSource, + liveIncidentsOptions: coreConfig.liveIncidentsConfig, + navigatorPredictionInterval: coreConfig.navigatorPredictionInterval, + statusUpdatingSettings: nil, + utilizeSensorData: coreConfig.utilizeSensorData, + historyDirectoryURL: coreConfig.historyRecordingConfig?.historyDirectoryURL, + initialManeuverAvoidanceRadius: coreConfig.routingConfig.initialManeuverAvoidanceRadius, + locale: coreConfig.locale + ) + + private lazy var _historyRecorder: HistoryRecording? = { + guard let historyDirectoryURL = coreConfig.historyRecordingConfig?.historyDirectoryURL else { + return nil + } + do { + let fileManager = FileManager.default + try fileManager.createDirectory(at: historyDirectoryURL, withIntermediateDirectories: true, attributes: nil) + } catch { + Log.error( + "Failed to create history saving directory at '\(historyDirectoryURL)' due to error: \(error)", + category: .settings + ) + return nil + } + return nativeHandlersFactory.historyRecorderHandle.map { + HistoryRecorder(handle: $0) + } + }() + + private lazy var _copilot: CopilotService? = { + guard let _historyRecorder else { return nil } + let version = onMainQueueSync { nativeHandlersFactory.navigator.native.version() } + return .init( + accessToken: coreConfig.credentials.navigation.accessToken, + navNativeVersion: version, + historyRecording: _historyRecorder, + isActive: coreConfig.copilotEnabled, + log: { logOutput in + Log.debug( + "\(logOutput)", + category: .copilot + ) + } + ) + }() + + var eventsMetadataProvider: EventsMetadataProvider { + onMainQueueSync { + let eventsMetadataProvider = EventsMetadataProvider( + appState: EventAppState(), + screen: .main, + device: .current + ) + eventsMetadataProvider.userInfo = coreConfig.telemetryAppMetadata?.configuration + return eventsMetadataProvider + } + } + + // Need to store the metadata provider and NN Telemetry + private var _sharedEventsManager: UnfairLocked = .init(nil) + + var movementMonitor: NavigationMovementMonitor { + _sharedMovementMonitor.mutate { _sharedMovementMonitor in + if let _sharedMovementMonitor { + return _sharedMovementMonitor + } + let movementMonitor = NavigationMovementMonitor() + _sharedMovementMonitor = movementMonitor + return movementMonitor + } + } + + private var _sharedMovementMonitor: UnfairLocked = .init(nil) +} + +// MARK: - MapboxNavigation implementation + +extension MapboxNavigationProvider: MapboxNavigation { + public func routingProvider() -> RoutingProvider { + if let customProvider = coreConfig.__customRoutingProvider { + return customProvider() + } + return MapboxRoutingProvider( + with: .init( + source: coreConfig.routingConfig.routingProviderSource, + nativeHandlersFactory: nativeHandlersFactory, + credentials: .init(coreConfig.credentials.navigation) + ) + ) + } + + public func tripSession() -> SessionController { + navigator() + } + + public func electronicHorizon() -> ElectronicHorizonController { + navigator() + } + + public func navigation() -> NavigationController { + navigator() + } + + public func eventsManager() -> NavigationEventsManager { + let telemetry = nativeHandlersFactory + .telemetry(eventsMetadataProvider: eventsMetadataProvider) + return _sharedEventsManager.mutate { _sharedEventsManager in + if let _sharedEventsManager { + return _sharedEventsManager + } + let eventsMetadataProvider = eventsMetadataProvider + let eventsManager = coreConfig.__customEventsManager?() ?? NavigationEventsManager( + eventsMetadataProvider: eventsMetadataProvider, + telemetry: telemetry + ) + _sharedEventsManager = eventsManager + return eventsManager + } + } + + public func historyRecorder() -> HistoryRecording? { + _historyRecorder + } + + public func copilot() -> CopilotService? { + _copilot + } +} + +extension TTSConfig { + @MainActor + fileprivate func speechSynthesizer( + with locale: Locale, + apiConfiguration: ApiConfiguration, + skuTokenProvider: SkuTokenProvider + ) -> SpeechSynthesizing { + let speechSynthesizer = switch self { + case .default: + MultiplexedSpeechSynthesizer( + mapboxSpeechApiConfiguration: apiConfiguration, + skuTokenProvider: skuTokenProvider.skuToken + ) + case .localOnly: + MultiplexedSpeechSynthesizer(speechSynthesizers: [SystemSpeechSynthesizer()]) + case .custom(let speechSynthesizer): + speechSynthesizer + } + speechSynthesizer.locale = locale + return speechSynthesizer + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/AlternativeRoute.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/AlternativeRoute.swift new file mode 100644 index 000000000..499c3eb8a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/AlternativeRoute.swift @@ -0,0 +1,175 @@ +import Foundation +import MapboxDirections +import MapboxNavigationNative +import Turf + +/// Additional reasonable routes besides the main roure that visit waypoints. +public struct AlternativeRoute: @unchecked Sendable { + let nativeRoute: RouteInterface + var isForkPointPassed: Bool = false + + /// A `Route` object that the current alternative route represents. + public let route: Route + + /// Alternative route identifier type + public typealias ID = UInt32 + /// Brief statistics of a route for traveling + public struct RouteInfo { + /// Expected travel distance + public let distance: LocationDistance + /// Expected travel duration + public let duration: TimeInterval + + public init(distance: LocationDistance, duration: TimeInterval) { + self.distance = distance + self.duration = duration + } + } + + /// Holds related indices values of an intersection. + public struct IntersectionGeometryIndices { + /// The leg index within a route + public let legIndex: Int + /// The geometry index of an intersection within leg geometry + public let legGeometryIndex: Int + /// The geometry index of an intersection within route geometry + public let routeGeometryIndex: Int + } + + /// Alternative route identificator. + /// + /// It is unique within the same navigation session. + public let id: ID + /// Unique route id. + public let routeId: RouteId + /// Intersection on the main route, where alternative route branches. + public let mainRouteIntersection: Intersection + /// Indices values of an intersection on the main route + public let mainRouteIntersectionIndices: IntersectionGeometryIndices + /// Intersection on the alternative route, where it splits from the main route. + public let alternativeRouteIntersection: Intersection + /// Indices values of an intersection on the alternative route + public let alternativeRouteIntersectionIndices: IntersectionGeometryIndices + /// Alternative route statistics, counting from the split point. + public let infoFromDeviationPoint: RouteInfo + /// Alternative route statistics, counting from it's origin. + public let infoFromOrigin: RouteInfo + /// The difference of distances between alternative and the main routes + public let distanceDelta: LocationDistance + /// The difference of expected travel time between alternative and the main routes + public let expectedTravelTimeDelta: TimeInterval + + public init?(mainRoute: Route, alternativeRoute nativeRouteAlternative: RouteAlternative) async { + guard let route = try? await nativeRouteAlternative.route.convertToDirectionsRoute() else { + return nil + } + + self.init(mainRoute: mainRoute, alternativeRoute: route, nativeRouteAlternative: nativeRouteAlternative) + } + + init?(mainRoute: Route, alternativeRoute: Route, nativeRouteAlternative: RouteAlternative) { + self.nativeRoute = nativeRouteAlternative.route + self.route = alternativeRoute + + self.id = nativeRouteAlternative.id + self.routeId = .init(rawValue: nativeRouteAlternative.route.getRouteId()) + + var legIndex = Int(nativeRouteAlternative.mainRouteFork.legIndex) + var segmentIndex = Int(nativeRouteAlternative.mainRouteFork.segmentIndex) + + self.mainRouteIntersectionIndices = .init( + legIndex: legIndex, + legGeometryIndex: segmentIndex, + routeGeometryIndex: Int(nativeRouteAlternative.mainRouteFork.geometryIndex) + ) + + guard let mainIntersection = mainRoute.findIntersection(on: legIndex, by: segmentIndex) else { + return nil + } + self.mainRouteIntersection = mainIntersection + + legIndex = Int(nativeRouteAlternative.alternativeRouteFork.legIndex) + segmentIndex = Int(nativeRouteAlternative.alternativeRouteFork.segmentIndex) + self.alternativeRouteIntersectionIndices = .init( + legIndex: legIndex, + legGeometryIndex: segmentIndex, + routeGeometryIndex: Int(nativeRouteAlternative.alternativeRouteFork.geometryIndex) + ) + + guard let alternativeIntersection = alternativeRoute.findIntersection(on: legIndex, by: segmentIndex) else { + return nil + } + self.alternativeRouteIntersection = alternativeIntersection + + self.infoFromDeviationPoint = .init( + distance: nativeRouteAlternative.infoFromFork.distance, + duration: nativeRouteAlternative.infoFromFork.duration + ) + self.infoFromOrigin = .init( + distance: nativeRouteAlternative.infoFromStart.distance, + duration: nativeRouteAlternative.infoFromStart.duration + ) + + self.distanceDelta = infoFromOrigin.distance - mainRoute.distance + self.expectedTravelTimeDelta = infoFromOrigin.duration - mainRoute.expectedTravelTime + } + + static func fromNative( + alternativeRoutes: [RouteAlternative], + relateveTo mainRoute: NavigationRoute + ) async -> [AlternativeRoute] { + var converted = [AlternativeRoute?](repeating: nil, count: alternativeRoutes.count) + await withTaskGroup(of: (Int, AlternativeRoute?).self) { group in + for (index, alternativeRoute) in alternativeRoutes.enumerated() { + group.addTask { + let alternativeRoute = await AlternativeRoute( + mainRoute: mainRoute.route, + alternativeRoute: alternativeRoute + ) + return (index, alternativeRoute) + } + } + + for await (index, alternativeRoute) in group { + guard let alternativeRoute else { + Log.error( + "Alternative routes parsing lost route with id: \(alternativeRoutes[index].route.getRouteId())", + category: .navigation + ) + continue + } + converted[index] = alternativeRoute + } + } + + return converted.compactMap { $0 } + } +} + +extension AlternativeRoute: Equatable { + public static func == (lhs: AlternativeRoute, rhs: AlternativeRoute) -> Bool { + return lhs.routeId == rhs.routeId && + lhs.route == rhs.route + } +} + +extension Route { + fileprivate func findIntersection(on legIndex: Int, by segmentIndex: Int) -> Intersection? { + guard legs.count > legIndex else { + return nil + } + + var leg = legs[legIndex] + guard let stepindex = leg.segmentRangesByStep.firstIndex(where: { $0.contains(segmentIndex) }) else { + return nil + } + + guard let intersectionIndex = leg.steps[stepindex].segmentIndicesByIntersection? + .firstIndex(where: { $0 == segmentIndex }) + else { + return nil + } + + return leg.steps[stepindex].intersections?[intersectionIndex] + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/BorderCrossing.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/BorderCrossing.swift new file mode 100644 index 000000000..0576e40e3 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/BorderCrossing.swift @@ -0,0 +1,32 @@ + +import Foundation +import MapboxDirections +import MapboxNavigationNative + +extension AdministrativeRegion { + init(_ adminInfo: AdminInfo) { + self.init(countryCode: adminInfo.iso_3166_1, countryCodeAlpha3: adminInfo.iso_3166_1_alpha3) + } +} + +/// ``BorderCrossing`` encapsulates a border crossing, specifying crossing region codes. +public struct BorderCrossing: Equatable { + public let from: AdministrativeRegion + public let to: AdministrativeRegion + + /// Initializes a new ``BorderCrossing`` object. + /// - Parameters: + /// - from: origin administrative region + /// - to: destination administrative region + public init(from: AdministrativeRegion, to: AdministrativeRegion) { + self.from = from + self.to = to + } + + init(_ borderCrossing: BorderCrossingInfo) { + self.init( + from: AdministrativeRegion(borderCrossing.from), + to: AdministrativeRegion(borderCrossing.to) + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/DistancedRoadObject.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/DistancedRoadObject.swift new file mode 100644 index 000000000..4a76bc531 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/DistancedRoadObject.swift @@ -0,0 +1,155 @@ +import CoreLocation +import Foundation +import MapboxNavigationNative + +/// Contains information about distance to the road object of a concrete type/shape (gantry, polygon, line, point etc.). +public enum DistancedRoadObject: Sendable, Equatable { + /// The information about distance to the road object represented as a point. + /// - Parameters: + /// - identifier: Road object identifier. + /// - kind: Road object kind. + /// - distance: Distance to the point object, measured in meters. + case point( + identifier: RoadObject.Identifier, + kind: RoadObject.Kind, + distance: CLLocationDistance + ) + + /// The information about distance to the road object represented as a gantry. + /// - Parameters: + /// - identifier: Road object identifier. + /// - kind: Road object kind. + /// - distance: Distance to the gantry object. + case gantry( + identifier: RoadObject.Identifier, + kind: RoadObject.Kind, + distance: CLLocationDistance + ) + + /// The information about distance to the road object represented as a polygon. + /// - Parameters: + /// - identifier: Road object identifier. + /// - kind: Road object kind. + /// - distanceToNearestEntry: Distance measured in meters to the nearest entry. + /// - distanceToNearestExit: Distance measured in meters to nearest exit. + /// - isInside: Boolean to indicate whether we're currently "inside" the object. + case polygon( + identifier: RoadObject.Identifier, + kind: RoadObject.Kind, + distanceToNearestEntry: CLLocationDistance?, + distanceToNearestExit: CLLocationDistance?, + isInside: Bool + ) + + /// The information about distance to the road object represented as a subgraph. + /// - Parameters: + /// - identifier: Road object identifier. + /// - kind: Road object kind. + /// - distanceToNearestEntry: Distance measured in meters to the nearest entry. + /// - distanceToNearestExit: Distance measured in meters to the nearest exit. + /// - isInside: Boolean that indicates whether we're currently "inside" the object. + case subgraph( + identifier: RoadObject.Identifier, + kind: RoadObject.Kind, + distanceToNearestEntry: CLLocationDistance?, + distanceToNearestExit: CLLocationDistance?, + isInside: Bool + ) + + /// The information about distance to the road object represented as a line. + /// - Parameters: + /// - identifier: Road object identifier. + /// - kind: Road object kind. + /// - distanceToEntry: Distance from the current position to entry point measured in meters along the road + /// graph. This value is 0 if already "within" the object. + /// - distanceToExit: Distance from the current position to the most likely exit point measured in meters along + /// the road graph. + /// - distanceToEnd: Distance from the current position to the most distance exit point measured in meters along + /// the road graph. + /// - isEntryFromStart: Boolean that indicates whether we enter the road object from its start. This value is + /// `false` if already "within" the object. + /// - length: Length of the road object measured in meters. + case line( + identifier: RoadObject.Identifier, + kind: RoadObject.Kind, + distanceToEntry: CLLocationDistance, + distanceToExit: CLLocationDistance, + distanceToEnd: CLLocationDistance, + isEntryFromStart: Bool, + length: CLLocationDistance + ) + + /// Road object identifier. + public var identifier: RoadObject.Identifier { + switch self { + case .point(let identifier, _, _), + .gantry(let identifier, _, _), + .polygon(let identifier, _, _, _, _), + .subgraph(let identifier, _, _, _, _), + .line(let identifier, _, _, _, _, _, _): + return identifier + } + } + + /// Road object kind. + public var kind: RoadObject.Kind { + switch self { + case .point(_, let type, _), + .gantry(_, let type, _), + .polygon(_, let type, _, _, _), + .subgraph(_, let type, _, _, _), + .line(_, let type, _, _, _, _, _): + return type + } + } + + init(_ native: MapboxNavigationNative.RoadObjectDistance) { + switch native.distanceInfo.type { + case .pointDistanceInfo: + let info = native.distanceInfo.getPointDistanceInfo() + self = .point( + identifier: native.roadObjectId, + kind: RoadObject.Kind(native.type), + distance: info.distance + ) + case .gantryDistanceInfo: + let info = native.distanceInfo.getGantryDistanceInfo() + self = .gantry( + identifier: native.roadObjectId, + kind: RoadObject.Kind(native.type), + distance: info.distance + ) + case .polygonDistanceInfo: + let info = native.distanceInfo.getPolygonDistanceInfo() + self = .polygon( + identifier: native.roadObjectId, + kind: RoadObject.Kind(native.type), + distanceToNearestEntry: info.entrances.first?.distance, + distanceToNearestExit: info.exits.first?.distance, + isInside: info.inside + ) + case .subGraphDistanceInfo: + let info = native.distanceInfo.getSubGraphDistanceInfo() + self = .subgraph( + identifier: native.roadObjectId, + kind: RoadObject.Kind(native.type), + distanceToNearestEntry: info.entrances.first?.distance, + distanceToNearestExit: info.exits.first?.distance, + isInside: info.inside + ) + case .lineDistanceInfo: + let info = native.distanceInfo.getLineDistanceInfo() + self = .line( + identifier: native.roadObjectId, + kind: RoadObject.Kind(native.type), + distanceToEntry: info.distanceToEntry, + distanceToExit: info.distanceToExit, + distanceToEnd: info.distanceToEnd, + isEntryFromStart: info.entryFromStart, + length: info.length + ) + @unknown default: + preconditionFailure("DistancedRoadObject can't be constructed. Unknown type.") + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/ElectronicHorizonConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/ElectronicHorizonConfig.swift new file mode 100644 index 000000000..646a32135 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/ElectronicHorizonConfig.swift @@ -0,0 +1,57 @@ +import CoreLocation +import Foundation +import MapboxNavigationNative + +/// Defines options for emitting ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition``, +/// ``Foundation/NSNotification/Name/electronicHorizonDidEnterRoadObject``, and +/// ``Foundation/NSNotification/Name/electronicHorizonDidExitRoadObject`` notifications while active guidance or free +/// drive is in progress. +/// +/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to +/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms +/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require +/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the +/// feature. +public struct ElectronicHorizonConfig: Equatable, Sendable { + /// The minimum length of the electronic horizon ahead of the current position, measured in meters. + public let length: CLLocationDistance + + /// The number of levels of branches by which to expand the horizon. + /// + /// A value of 0 results in only the most probable path (MPP). A value of 1 adds paths branching out directly from + /// the MPP, a value of 2 adds paths branching out from those paths, and so on. Only 0, 1, and 2 are usable in terms + /// of performance. + public let expansionLevel: UInt + + /// Minimum length of side branches, measured in meters. + public let branchLength: CLLocationDistance + + /// Minimum time which should pass between consecutive navigation statuses to update electronic horizon (seconds). + /// If `nil` we update electronic horizon on each navigation status. + public let minimumTimeIntervalBetweenUpdates: TimeInterval? + + public init( + length: CLLocationDistance, + expansionLevel: UInt, + branchLength: CLLocationDistance, + minTimeDeltaBetweenUpdates: TimeInterval? + ) { + self.length = length + self.expansionLevel = expansionLevel + self.branchLength = branchLength + self.minimumTimeIntervalBetweenUpdates = minTimeDeltaBetweenUpdates + } +} + +extension MapboxNavigationNative.ElectronicHorizonOptions { + convenience init(_ options: ElectronicHorizonConfig) { + self.init( + length: options.length, + expansion: UInt8(options.expansionLevel), + branchLength: options.branchLength, + doNotRecalculateInUncertainState: true, + minTimeDeltaBetweenUpdates: options.minimumTimeIntervalBetweenUpdates as NSNumber?, + alertsService: nil + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Interchange.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Interchange.swift new file mode 100644 index 000000000..782109c14 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Interchange.swift @@ -0,0 +1,32 @@ +import Foundation +import MapboxNavigationNative + +/// Contains information about routing and passing interchange along the route. +public struct Interchange: Equatable { + /// Interchange identifier, if available. + public var identifier: String + /// The localized names of the interchange, if available. + public let names: [LocalizedRoadObjectName] + + /// Initializes a new `Interchange` object. + /// - Parameters: + /// - names: The localized names of the interchange. + public init(names: [LocalizedRoadObjectName]) { + self.identifier = "" + self.names = names + } + + /// Initializes a new `Interchange` object. + /// - Parameters: + /// - identifier: Interchange identifier. + /// - names: The localized names of the interchange. + public init(identifier: String, names: [LocalizedRoadObjectName]) { + self.identifier = identifier + self.names = names + } + + init(_ icInfo: IcInfo) { + let names = icInfo.name.map { LocalizedRoadObjectName($0) } + self.init(identifier: icInfo.id, names: names) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Junction.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Junction.swift new file mode 100644 index 000000000..dbb36212b --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Junction.swift @@ -0,0 +1,32 @@ +import Foundation +import MapboxNavigationNative + +/// Contains information about routing and passing junction along the route. +public struct Junction: Equatable { + /// Junction identifier, if available. + public var identifier: String + /// The localized names of the junction, if available. + public let names: [LocalizedRoadObjectName] + + /// Initializes a new `Junction` object. + /// - Parameters: + /// - names: The localized names of the interchange. + public init(names: [LocalizedRoadObjectName]) { + self.identifier = "" + self.names = names + } + + /// Initializes a new `Junction` object. + /// - Parameters: + /// - identifier: Junction identifier. + /// - names: The localized names of the interchange. + public init(identifier: String, names: [LocalizedRoadObjectName]) { + self.identifier = identifier + self.names = names + } + + init(_ jctInfo: JctInfo) { + let names = jctInfo.name.map { LocalizedRoadObjectName($0) } + self.init(identifier: jctInfo.id, names: names) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/LocalizedRoadObjectName.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/LocalizedRoadObjectName.swift new file mode 100644 index 000000000..f076d1358 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/LocalizedRoadObjectName.swift @@ -0,0 +1,24 @@ +import Foundation +import MapboxNavigationNative + +/// Road object information, like interchange name. +public struct LocalizedRoadObjectName: Equatable { + /// 2 letters language code, e.g. en or ja. + public let language: String + + /// The name of the road object. + public let text: String + + /// Initializes a new `LocalizedRoadObjectName` object. + /// - Parameters: + /// - language: 2 letters language code, e.g. en or ja. + /// - text: The name of the road object. + public init(language: String, text: String) { + self.language = language + self.text = text + } + + init(_ localizedString: LocalizedString) { + self.init(language: localizedString.language, text: localizedString.value) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRIdentifier.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRIdentifier.swift new file mode 100644 index 000000000..31e859922 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRIdentifier.swift @@ -0,0 +1,32 @@ +import Foundation +import MapboxNavigationNative + +/// Identifies a road object according to one of two OpenLR standards. +/// +/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to +/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms +/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require +/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the +/// feature. +public enum OpenLRIdentifier { + /// [TomTom OpenLR](http://www.openlr.org/). + /// + /// Supported references: line location, point along line, polygon. + case tomTom(reference: RoadObject.Identifier) + + /// TPEG OpenLR. + /// + /// Only line locations are supported. + case tpeg(reference: RoadObject.Identifier) +} + +extension MapboxNavigationNative.Standard { + init(identifier: OpenLRIdentifier) { + switch identifier { + case .tomTom: + self = .tomTom + case .tpeg: + self = .TPEG + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLROrientation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLROrientation.swift new file mode 100644 index 000000000..3152ddf37 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLROrientation.swift @@ -0,0 +1,38 @@ +import Foundation +import MapboxNavigationNative + +/// Describes the relationship between the road object and the direction of a eferenced line. The road object may be +/// directed in the same direction as the line, against that direction, both directions, or the direction of the road +/// object might be unknown. +/// +/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to +/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms +/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require +/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the +/// feature. +public enum OpenLROrientation: Equatable, Sendable { + /// The relationship between the road object and the direction of the referenced line is unknown. + case unknown + /// The road object is directed in the same direction as the referenced line. + case alongLine + /// The road object is directed against the direction of the referenced line. + case againstLine + /// The road object is directed in both directions. + case both + + init(_ native: MapboxNavigationNative.Orientation) { + switch native { + case .noOrientationOrUnknown: + self = .unknown + case .withLineDirection: + self = .alongLine + case .againstLineDirection: + self = .againstLine + case .both: + self = .both + @unknown default: + assertionFailure("Unknown OpenLROrientation type.") + self = .unknown + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRSideOfRoad.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRSideOfRoad.swift new file mode 100644 index 000000000..6174bdaa1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRSideOfRoad.swift @@ -0,0 +1,38 @@ +import Foundation +import MapboxNavigationNative + +/// Describes the relationship between the road object and the road. +/// The road object can be on the right side of the road, on the left side of the road, on both sides of the road or +/// directly on the road. +/// +/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to +/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms +/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require +/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the +/// feature. +public enum OpenLRSideOfRoad: Equatable, Sendable { + /// The relationship between the road object and the road is unknown. + case unknown + /// The road object is on the right side of the road. + case right + /// The road object is on the left side of the road. + case left + /// The road object is on both sides of the road or directly on the road. + case both + + init(_ native: MapboxNavigationNative.SideOfRoad) { + switch native { + case .onRoadOrUnknown: + self = .unknown + case .right: + self = .right + case .left: + self = .left + case .both: + self = .both + @unknown default: + assertionFailure("Unknown OpenLRSideOfRoad value.") + self = .unknown + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraph.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraph.swift new file mode 100644 index 000000000..3feb72d3e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraph.swift @@ -0,0 +1,70 @@ +import Foundation +import MapboxNavigationNative +import Turf + +/// ``RoadGraph`` provides methods to get edge shape (e.g. ``RoadGraph/Edge``) and metadata. +/// +/// You do not create a ``RoadGraph`` object manually. Instead, use the ``RoadMatching/roadGraph`` from +/// ``ElectronicHorizonController/roadMatching`` +/// +/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to +/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms +/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require +/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the +/// feature. +public final class RoadGraph: Sendable { + // MARK: Getting Edge Info + + /// Returns metadata about the edge with the given edge identifier. + /// - Parameter edgeIdentifier: The identifier of the edge to query. + /// - Returns: Metadata about the edge with the given edge identifier, or `nil` if the edge is not in the cache. + public func edgeMetadata(edgeIdentifier: Edge.Identifier) -> Edge.Metadata? { + if let edgeMetadata = native.getEdgeMetadata(forEdgeId: UInt64(edgeIdentifier)) { + return Edge.Metadata(edgeMetadata) + } + return nil + } + + /// Returns a line string geometry corresponding to the given edge identifier. + /// + /// - Parameter edgeIdentifier: The identifier of the edge to query. + /// - Returns: A line string corresponding to the given edge identifier, or `nil` if the edge is not in the cache. + public func edgeShape(edgeIdentifier: Edge.Identifier) -> LineString? { + guard let locations = native.getEdgeShape(forEdgeId: UInt64(edgeIdentifier)) else { + return nil + } + return LineString(locations.map(\.value)) + } + + // MARK: Retrieving the Shape of an Object + + /// Returns a line string geometry corresponding to the given path. + /// + /// - Parameter path: The path of the geometry. + /// - Returns: A line string corresponding to the given path, or `nil` if any of path edges are not in the cache. + public func shape(of path: Path) -> LineString? { + guard let locations = native.getPathShape(for: GraphPath(path)) else { + return nil + } + return LineString(locations.map(\.value)) + } + + /// Returns a point corresponding to the given position. + /// + /// - Parameter position: The position of the point. + /// - Returns: A point corresponding to the given position, or `nil` if the edge is not in the cache. + public func shape(of position: Position) -> Point? { + guard let location = native.getPositionCoordinate(for: GraphPosition(position)) else { + return nil + } + return Point(location.value) + } + + init(_ native: GraphAccessor) { + self.native = native + } + + private let native: GraphAccessor +} + +extension GraphAccessor: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdge.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdge.swift new file mode 100644 index 000000000..a2fe32038 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdge.swift @@ -0,0 +1,73 @@ +import Foundation +import MapboxNavigationNative + +extension RoadGraph { + /// An edge in a routing graph. For example, an edge may represent a road segment between two intersections or + /// between the two ends of a bridge. An edge may traverse multiple road objects, and a road object may be + /// associated with multiple edges. + /// + /// An electronic horizon is a probable path (or paths) of a vehicle. The road network ahead of the user is + /// represented as a tree of edges. Each intersection has outlet edges. In turn, each edge has a probability of + /// transition to another edge, as well as details about the road segment that the edge traverses. You can use these + /// details to influence application behavior based on predicted upcoming conditions. + /// + /// During active turn-by-turn navigation, the user-selected route and its metadata influence the path of the + /// electronic horizon. During passive navigation (free-driving), no route is actively selected, so the SDK will + /// determine the most probable path from the vehicle’s current location. You can receive notifications about + /// changes in the current state of the electronic horizon by observing the + /// ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition``, + /// ``Foundation/NSNotification/Name/electronicHorizonDidEnterRoadObject``, and + /// ``Foundation/NSNotification/Name/electronicHorizonDidExitRoadObject`` notifications. + /// + /// Use a ``RoadGraph`` object to get an edge with a given identifier. + /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to + /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox + /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and + /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level + /// of use of the feature. + public struct Edge: Equatable, Sendable { + /// Unique identifier of a directed edge. + /// + /// Use a ``RoadGraph`` object to get more information about the edge with a given identifier. + public typealias Identifier = UInt + + /// Unique identifier of the directed edge. + public let identifier: Identifier + + /// The level of the edge. + /// + /// A value of 0 indicates that the edge is part of the most probable path (MPP), a value of 1 indicates an edge + /// that branches away from the MPP, and so on. + public let level: UInt + + /// The probability that the user will transition onto this edge, with 1 being certain and 0 being unlikely. + public let probability: Double + + /// The edges to which the user could transition from this edge. + /// + /// The most probable path may be split at some point if some of edges have a low probability difference + /// (±0.05). For example, ``RoadGraph/Edge/outletEdges`` can contain more than one edge with + /// ``RoadGraph/Edge/level`` set to 0. Currently, there is a maximum limit of one split per electronic horizon. + public let outletEdges: [Edge] + + /// Initializes a new ``RoadGraph/Edge`` object. + /// - Parameters: + /// - identifier: The unique identifier of a directed edge.: + /// - level: The level of the edge.: + /// - probability: The probability that the user will transition onto this edge.: + /// - outletEdges: The edges to which the user could transition from this edge. + public init(identifier: Identifier, level: UInt, probability: Double, outletEdges: [Edge]) { + self.identifier = identifier + self.level = level + self.probability = probability + self.outletEdges = outletEdges + } + + init(_ native: ElectronicHorizonEdge) { + self.identifier = UInt(native.id) + self.level = UInt(native.level) + self.probability = native.probability + self.outletEdges = native.out.map(Edge.init) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdgeMetadata.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdgeMetadata.swift new file mode 100644 index 000000000..2363f8a05 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdgeMetadata.swift @@ -0,0 +1,195 @@ +import CoreLocation +import Foundation +import MapboxDirections +import MapboxNavigationNative + +extension RoadGraph.Edge { + /// Indicates how many directions the user may travel along an edge. + /// + /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to + /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox + /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and + /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level + /// of use of the feature. + public enum Directionality: Sendable { + /// The user may only travel in one direction along the edge. + case oneWay + /// The user may travel in either direction along the edge. + case bothWays + } + + /// Edge metadata + public struct Metadata: Sendable { + // MARK: Geographical & Physical Characteristics + + /// The bearing in degrees clockwise at the start of the edge. + public let heading: CLLocationDegrees + + /// The edge’s length in meters. + public let length: CLLocationDistance + + /// The edge’s mean elevation, measured in meters. + public let altitude: CLLocationDistance? + + /// The edge’s curvature. + public let curvature: UInt + + // MARK: Road Classification + + /// Is the edge a bridge? + public let isBridge: Bool + + /// The edge’s general road classes. + public let roadClasses: RoadClasses + + /// The edge’s functional road class, according to the [Mapbox Streets + /// source](https://docs.mapbox.com/vector-tiles/reference/mapbox-streets-v8/#road), version 8. + public let mapboxStreetsRoadClass: MapboxStreetsRoadClass + + // MARK: Legal definitions + + /// The edge's names + public let names: [RoadName] + + /// The ISO 3166-1 alpha-2 code of the country where this edge is located. + public let countryCode: String? + + /// The ISO 3166-2 code of the country subdivision where this edge is located. + public let regionCode: String? + + // MARK: Road Regulations + + /// Indicates how many directions the user may travel along the edge. + public let directionality: Directionality + + /// The edge’s maximum speed limit. + public let speedLimit: Measurement? + + /// The user’s expected average speed along the edge, measured in meters per second. + public let speed: CLLocationSpeed + + /// Indicates which side of a bidirectional road on which the driver must be driving. Also referred to as the + /// rule of the road. + public let drivingSide: DrivingSide + + /// The number of parallel traffic lanes along the edge. + public let laneCount: UInt? + + /// `true` if edge is considered to be in an urban area, `false` otherwise. + public let isUrban: Bool + + /// Initializes a new edge ``RoadGraph/Edge/Metadata`` object. + public init( + heading: CLLocationDegrees, + length: CLLocationDistance, + roadClasses: RoadClasses, + mapboxStreetsRoadClass: MapboxStreetsRoadClass, + speedLimit: Measurement?, + speed: CLLocationSpeed, + isBridge: Bool, + names: [RoadName], + laneCount: UInt?, + altitude: CLLocationDistance?, + curvature: UInt, + countryCode: String?, + regionCode: String?, + drivingSide: DrivingSide, + directionality: Directionality, + isUrban: Bool + ) { + self.heading = heading + self.length = length + self.roadClasses = roadClasses + self.mapboxStreetsRoadClass = mapboxStreetsRoadClass + self.speedLimit = speedLimit + self.speed = speed + self.isBridge = isBridge + self.names = names + self.laneCount = laneCount + self.altitude = altitude + self.curvature = curvature + self.countryCode = countryCode + self.regionCode = regionCode + self.drivingSide = drivingSide + self.directionality = directionality + self.isUrban = isUrban + } + + /// Initializes a new edge ``RoadGraph/Edge/Metadata`` object. + init( + heading: CLLocationDegrees, + length: CLLocationDistance, + roadClasses: RoadClasses, + mapboxStreetsRoadClass: MapboxStreetsRoadClass, + speedLimit: Measurement?, + speed: CLLocationSpeed, + isBridge: Bool, + names: [RoadName], + laneCount: UInt?, + altitude: CLLocationDistance?, + curvature: UInt, + countryCode: String?, + regionCode: String?, + drivingSide: DrivingSide, + directionality: Directionality + ) { + self.init( + heading: heading, + length: length, + roadClasses: roadClasses, + mapboxStreetsRoadClass: mapboxStreetsRoadClass, + speedLimit: speedLimit, + speed: speed, + isBridge: isBridge, + names: names, + laneCount: laneCount, + altitude: altitude, + curvature: curvature, + countryCode: countryCode, + regionCode: regionCode, + drivingSide: drivingSide, + directionality: directionality, + isUrban: false + ) + } + + init(_ native: EdgeMetadata) { + self.heading = native.heading + self.length = native.length + self.mapboxStreetsRoadClass = MapboxStreetsRoadClass(native.frc, isRamp: native.ramp) + if let speedLimitValue = native.speedLimit as? Double { + // TODO: Convert to miles per hour as locally appropriate. + self.speedLimit = Measurement( + value: speedLimitValue == 0.0 ? .infinity : speedLimitValue, + unit: UnitSpeed.metersPerSecond + ).converted(to: .kilometersPerHour) + } else { + self.speedLimit = nil + } + self.speed = native.speed + + var roadClasses: RoadClasses = [] + if native.motorway { + roadClasses.update(with: .motorway) + } + if native.tunnel { + roadClasses.update(with: .tunnel) + } + if native.toll { + roadClasses.update(with: .toll) + } + self.roadClasses = roadClasses + + self.isBridge = native.bridge + self.names = native.names.compactMap(RoadName.init) + self.laneCount = native.laneCount as? UInt + self.altitude = native.meanElevation as? Double + self.curvature = UInt(native.curvature) + self.countryCode = native.countryCodeIso2 + self.regionCode = native.stateCode + self.drivingSide = native.isRightHandTraffic ? .right : .left + self.directionality = native.isOneway ? .oneWay : .bothWays + self.isUrban = native.isUrban + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPath.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPath.swift new file mode 100644 index 000000000..65b631f53 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPath.swift @@ -0,0 +1,66 @@ +import CoreLocation +import Foundation +import MapboxNavigationNative + +extension RoadGraph { + /// A position along a linear object in the road graph. + /// + /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to + /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox + /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and + /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level + /// of use of the feature. + public struct Path: Equatable, Sendable { + /// The edge identifiers that fully or partially coincide with the linear object. + public let edgeIdentifiers: [Edge.Identifier] + + /// The distance from the start of an edge to the start of the linear object as a fraction of the edge’s length + /// from 0 to 1. + public let fractionFromStart: Double + + /// The distance from the end of the linear object to the end of an edge as a fraction of the edge’s length from + /// 0 to 1. + public let fractionToEnd: Double + + /// Length of a path, measured in meters. + public let length: CLLocationDistance + + /// Initializes a new ``RoadGraph/Path`` object. + /// - Parameters: + /// - edgeIdentifiers: An `Array` of edge identifiers that fully or partially coincide with the linear object. + /// - fractionFromStart: The distance from the start of an edge to the start of the linear object as a + /// fraction of the edge's length from 0 to 1. + /// - fractionToEnd: The distance from the end of the linear object to the edge of the edge as a fraction of + /// the edge's length from 0 to 1. + /// - length: Length of a ``RoadGraph/Path`` measured in meters. + public init( + edgeIdentifiers: [RoadGraph.Edge.Identifier], + fractionFromStart: Double, + fractionToEnd: Double, + length: CLLocationDistance + ) { + self.edgeIdentifiers = edgeIdentifiers + self.fractionFromStart = fractionFromStart + self.fractionToEnd = fractionToEnd + self.length = length + } + + init(_ native: GraphPath) { + self.edgeIdentifiers = native.edges.map(\.uintValue) + self.fractionFromStart = native.percentAlongBegin + self.fractionToEnd = native.percentAlongEnd + self.length = native.length + } + } +} + +extension GraphPath { + convenience init(_ path: RoadGraph.Path) { + self.init( + edges: path.edgeIdentifiers.map { NSNumber(value: $0) }, + percentAlongBegin: path.fractionFromStart, + percentAlongEnd: path.fractionToEnd, + length: path.length + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPosition.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPosition.swift new file mode 100644 index 000000000..325e0e98a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPosition.swift @@ -0,0 +1,42 @@ +import Foundation +import MapboxNavigationNative + +extension RoadGraph { + /// The position of a point object in the road graph. + /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to + /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox + /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and + /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level + /// of use of the feature. + public struct Position: Equatable, Sendable { + /// The edge identifier along which the point object lies. + public let edgeIdentifier: Edge.Identifier + + /// The distance from the start of an edge to the point object as a fraction of the edge’s length from 0 to 1. + public let fractionFromStart: Double + + /// Initializes a new ``RoadGraph/Position`` object with a given edge identifier and fraction from the start of + /// the edge. + /// - Parameters: + /// - edgeIdentifier: The edge identifier. + /// - fractionFromStart: The fraction from the start of the edge. + public init(edgeIdentifier: RoadGraph.Edge.Identifier, fractionFromStart: Double) { + self.edgeIdentifier = edgeIdentifier + self.fractionFromStart = fractionFromStart + } + + init(_ native: GraphPosition) { + self.edgeIdentifier = UInt(native.edgeId) + self.fractionFromStart = native.percentAlong + } + } +} + +extension GraphPosition { + convenience init(_ position: RoadGraph.Position) { + self.init( + edgeId: UInt64(position.edgeIdentifier), + percentAlong: position.fractionFromStart + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadName.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadName.swift new file mode 100644 index 000000000..4a63c835c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadName.swift @@ -0,0 +1,42 @@ +import Foundation +import MapboxNavigationNative + +/// Road information, like Route number, street name, shield information, etc. +/// +/// - note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta +/// and is subject to changes, including its pricing. Use of the feature is subject to the beta product restrictions +/// in the Mapbox Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at +/// any time and require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of +/// the level of use of the feature. +public struct RoadName: Equatable, Sendable { + /// The name of the road. + /// + /// If you display a name to the user, you may need to abbreviate common words like “East” or “Boulevard” to ensure + /// that it fits in the allotted space. + public let text: String + + /// IETF BCP 47 language tag or "Unspecified" or empty string. + public let language: String + + /// Shield information of the road. + public let shield: RoadShield? + + /// Creates a new `RoadName` instance. + /// - Parameters: + /// - text: The name of the road. + /// - language: IETF BCP 47 language tag or "Unspecified" or empty string. + /// - shield: Shield information of the road. + public init(text: String, language: String, shield: RoadShield? = nil) { + self.text = text + self.language = language + self.shield = shield + } + + init?(_ native: MapboxNavigationNative.RoadName) { + guard native.text != "/" else { return nil } + + self.shield = native.shield.map(RoadShield.init) + self.text = native.text + self.language = native.language + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObject.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObject.swift new file mode 100644 index 000000000..5b8224cd3 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObject.swift @@ -0,0 +1,78 @@ +import CoreLocation +import Foundation +@preconcurrency import MapboxNavigationNative + +public struct RoadObjectAhead: Equatable, Sendable { + public var roadObject: RoadObject + public var distance: CLLocationDistance? + + public init(roadObject: RoadObject, distance: CLLocationDistance? = nil) { + self.roadObject = roadObject + self.distance = distance + } +} + +/// Describes the object on the road. +/// There are two sources of road objects: active route and the electronic horizon. +public struct RoadObject: Equatable, Sendable { + /// Identifier of the road object. If we get the same objects (e.g. ``RoadObject/Kind/tunnel(_:)``) from the + /// electronic horizon and the active route, they will not have the same IDs. + public let identifier: RoadObject.Identifier + + /// Length of the object, `nil` if the object is point-like. + public let length: CLLocationDistance? + + /// Location of the road object. + public let location: RoadObject.Location + + /// Kind of the road object with metadata. + public let kind: RoadObject.Kind + + /// `true` if an object is added by user, `false` if it comes from Mapbox service. + public let isUserDefined: Bool + + /// Indicates whether the road object is located in an urban area. + /// This property is set to `nil` if the road object comes from a call to the + /// ``RoadObjectStore/roadObject(identifier:)`` method and ``RoadObject/location`` is set to + /// ``RoadObject/Location/point(position:)``. + public let isUrban: Bool? + + let native: MapboxNavigationNative.RoadObject? + + /// Initializes a new `RoadObject` object. + public init( + identifier: RoadObject.Identifier, + length: CLLocationDistance?, + location: RoadObject.Location, + kind: RoadObject.Kind, + isUrban: Bool? + ) { + self.identifier = identifier + self.length = length + self.location = location + self.kind = kind + self.isUserDefined = true + self.isUrban = isUrban + self.native = nil + } + + /// Initializes a new ``RoadObject`` object. + init( + identifier: RoadObject.Identifier, + length: CLLocationDistance?, + location: RoadObject.Location, + kind: RoadObject.Kind + ) { + self.init(identifier: identifier, length: length, location: location, kind: kind, isUrban: nil) + } + + public init(_ native: MapboxNavigationNative.RoadObject) { + self.identifier = native.id + self.length = native.length?.doubleValue + self.location = RoadObject.Location(native.location) + self.kind = RoadObject.Kind(type: native.type, metadata: native.metadata) + self.isUserDefined = native.provider == .custom + self.isUrban = native.isUrban?.boolValue + self.native = native + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectEdgeLocation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectEdgeLocation.swift new file mode 100644 index 000000000..447b84fa1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectEdgeLocation.swift @@ -0,0 +1,38 @@ +import Foundation +import MapboxNavigationNative + +extension RoadObject { + /// Represents location of road object on road graph. + /// + /// A point object is represented by a single edge whose location has the same ``fractionFromStart`` and + /// ``fractionToEnd``. + /// + /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to + /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox + /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and + /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level + /// of use of the feature. + public struct EdgeLocation { + /// Offset from the start of edge (0 - 1) pointing to the beginning of road object on this edge will be 0 for + /// all edges in the line-like road object except the very first one in the case of point-like object + /// fractionFromStart == fractionToEnd. + public let fractionFromStart: Double + + /// Offset from the start of edge (0 - 1) pointing to the end of road object on this edge will be 1 for all + /// edges in the line-like road object except the very first one in the case of point-like object + /// fractionFromStart == fractionToEnd. + public let fractionToEnd: Double + + /// Initializes a new ``RoadObject/EdgeLocation`` object with a fraction from the start and a fraction from the + /// end of the road object. + public init(fractionFromStart: Double, fractionToEnd: Double) { + self.fractionFromStart = fractionFromStart + self.fractionToEnd = fractionToEnd + } + + init(_ native: MapboxNavigationNative.RoadObjectEdgeLocation) { + self.fractionFromStart = native.percentAlongBegin + self.fractionToEnd = native.percentAlongEnd + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectKind.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectKind.swift new file mode 100644 index 000000000..d6a06b7b2 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectKind.swift @@ -0,0 +1,124 @@ +import Foundation +import MapboxDirections +import MapboxNavigationNative + +extension RoadObject { + /// Type of the road object. + public enum Kind: Equatable, @unchecked Sendable { + /// An alert providing information about incidents on a route. Incidents can include *congestion*, + /// *massTransit*, and more (see `Incident.Kind` for the full list of incident types). + case incident(Incident?) + + /// An alert describing a point along the route where a toll may be collected. Note that this does not describe + /// the entire toll road, rather it describes a booth or electronic gate where a toll is typically charged. + case tollCollection(TollCollection?) + + /// An alert describing a country border crossing along the route. The alert triggers at the point where the + /// administrative boundary changes from one country to another. Two-letter and three-letter ISO 3166-1 country + /// codes are provided for the exiting country and the entering country. See ``BorderCrossing``. + case borderCrossing(BorderCrossing?) + + /// An alert describing a section of the route that continues through a tunnel. The alert begins at the entrance + /// of the tunnel and ends at the exit of the tunnel. For named tunnels, the tunnel name is provided as part of + /// ``Tunnel/name``. + case tunnel(Tunnel?) + + /// An alert about a rest area or service area accessible from the route. The alert marks the point along the + /// route where a driver can choose to pull off to access a rest stop. See `MapboxDirections.StopType`. + case serviceArea(RestStop?) + + /// An alert about a segment of a route that includes a restriction. Restricted roads can include private + /// access roads or gated areas that can be accessed but are not open to vehicles passing through. + case restrictedArea + + /// An alert about a segment of a route that includes a bridge. + case bridge + + /// An alert about a railroad crossing at grade, also known as a level crossing. + case railroadCrossing + + /// A road alert that was added by the user via ``RoadObjectStore/addUserDefinedRoadObject(_:)``, + case userDefined + + /// Japan-specific interchange info, refers to an expressway entrance and exit, e.g. Wangannarashino IC. + case ic(Interchange?) + + /// Japan-specific junction info, refers to a place where multiple expressways meet, e.g. Ariake JCT. + case jct(Junction?) + + /// Undefined. + case undefined + + init(_ native: MapboxNavigationNative.RoadObjectType) { + switch native { + case .incident: + self = .incident(nil) + case .tollCollectionPoint: + self = .tollCollection(nil) + case .borderCrossing: + self = .borderCrossing(nil) + case .tunnel: + self = .tunnel(nil) + case .serviceArea: + self = .serviceArea(nil) + case .restrictedArea: + self = .restrictedArea + case .bridge: + self = .bridge + case .railwayCrossing: + self = .railroadCrossing + case .custom: + self = .userDefined + case .ic: + self = .ic(nil) + case .jct: + self = .jct(nil) + case .notification: + self = .undefined + case .mergingArea: + self = .undefined + @unknown default: + self = .undefined + } + } + + init(type: MapboxNavigationNative.RoadObjectType, metadata: MapboxNavigationNative.RoadObjectMetadata) { + switch type { + case .incident: + self = .incident(metadata.isIncidentInfo() ? Incident(metadata.getIncidentInfo()) : nil) + case .tollCollectionPoint: + self = .tollCollection( + metadata + .isTollCollectionInfo() ? TollCollection(metadata.getTollCollectionInfo()) : nil + ) + case .borderCrossing: + self = .borderCrossing( + metadata + .isBorderCrossingInfo() ? BorderCrossing(metadata.getBorderCrossingInfo()) : nil + ) + case .tunnel: + self = .tunnel(metadata.isTunnelInfo() ? Tunnel(metadata.getTunnelInfo()) : nil) + case .serviceArea: + self = .serviceArea(metadata.isServiceAreaInfo() ? RestStop(metadata.getServiceAreaInfo()) : nil) + case .restrictedArea: + self = .restrictedArea + case .bridge: + self = .bridge + case .railwayCrossing: + self = .railroadCrossing + case .custom: + self = .userDefined + case .ic: + self = .ic(metadata.isIcInfo() ? Interchange(metadata.getIcInfo()) : nil) + case .jct: + self = .jct(metadata.isJctInfo() ? Junction(metadata.getJctInfo()) : nil) + case .notification: + self = .undefined + case .mergingArea: + self = .undefined + @unknown default: + self = .undefined + } + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectLocation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectLocation.swift new file mode 100644 index 000000000..b4c462b14 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectLocation.swift @@ -0,0 +1,127 @@ +import Foundation +import MapboxNavigationNative +import Turf + +extension RoadObject { + /// The location of a road object in the road graph. + public enum Location: Equatable, Sendable { + /// Location of an object represented as a gantry. + /// - Parameters: + /// - positions: Positions of gantry entries. + /// - shape: Shape of a gantry. + case gantry(positions: [RoadObject.Position], shape: Turf.Geometry) + + /// Location of an object represented as a point. + /// - position: Position of the object on the road graph. + case point(position: RoadObject.Position) + + /// Location of an object represented as a polygon. + /// - Parameters: + /// - entries: Positions of polygon entries. + /// - exits: Positions of polygon exits. + /// - shape: Shape of a polygon. + case polygon( + entries: [RoadObject.Position], + exits: [RoadObject.Position], + shape: Turf.Geometry + ) + + /// Location of an object represented as a polyline. + /// - Parameters: + /// - path: Position of a polyline on a road graph. + /// - shape: Shape of a polyline. + case polyline(path: RoadGraph.Path, shape: Turf.Geometry) + + /// Location of an object represented as a subgraph. + /// - Parameters: + /// - enters: Positions of the subgraph enters. + /// - exits: Positions of the subgraph exits. + /// - shape: Shape of a subgraph. + /// - edges: Edges of the subgraph associated by id. + case subgraph( + enters: [RoadObject.Position], + exits: [RoadObject.Position], + shape: Turf.Geometry, + edges: [RoadGraph.SubgraphEdge.Identifier: RoadGraph.SubgraphEdge] + ) + + /// Location of an object represented as an OpenLR line. + /// - Parameters: + /// - path: Position of a line on a road graph. + /// - shape: Shape of a line. + case openLRLine(path: RoadGraph.Path, shape: Turf.Geometry) + + /// Location of an object represented as an OpenLR point. + /// - Parameters: + /// - position: Position of the point on the graph. + /// - sideOfRoad: Specifies on which side of road the point is located. + /// - orientation: Specifies orientation of the object relative to referenced line. + /// - coordinate: Map coordinate of the point. + case openLRPoint( + position: RoadGraph.Position, + sideOfRoad: OpenLRSideOfRoad, + orientation: OpenLROrientation, + coordinate: CLLocationCoordinate2D + ) + + /// Location of a route alert. + /// - Parameter shape: Shape of an object. + case routeAlert(shape: Turf.Geometry) + + init(_ native: MapboxNavigationNative.MatchedRoadObjectLocation) { + switch native.type { + case .openLRLineLocation: + let location = native.getOpenLRLineLocation() + self = .openLRLine( + path: RoadGraph.Path(location.getPath()), + shape: Geometry(location.getShape()) + ) + case .openLRPointAlongLineLocation: + let location = native.getOpenLRPointAlongLineLocation() + self = .openLRPoint( + position: RoadGraph.Position(location.getPosition()), + sideOfRoad: OpenLRSideOfRoad(location.getSideOfRoad()), + orientation: OpenLROrientation(location.getOrientation()), + coordinate: location.getCoordinate() + ) + case .matchedPolylineLocation: + let location = native.getMatchedPolylineLocation() + self = .polyline( + path: RoadGraph.Path(location.getPath()), + shape: Geometry(location.getShape()) + ) + case .matchedGantryLocation: + let location = native.getMatchedGantryLocation() + self = .gantry( + positions: location.getPositions().map(RoadObject.Position.init), + shape: Geometry(location.getShape()) + ) + case .matchedPolygonLocation: + let location = native.getMatchedPolygonLocation() + self = .polygon( + entries: location.getEntries().map(RoadObject.Position.init), + exits: location.getExits().map(RoadObject.Position.init), + shape: Geometry(location.getShape()) + ) + case .matchedPointLocation: + let location = native.getMatchedPointLocation() + self = .point(position: RoadObject.Position(location.getPosition())) + case .matchedSubgraphLocation: + let location = native.getMatchedSubgraphLocation() + let edges = location.getEdges() + .map { id, edge in (UInt(truncating: id), RoadGraph.SubgraphEdge(edge)) } + self = .subgraph( + enters: location.getEnters().map(RoadObject.Position.init), + exits: location.getExits().map(RoadObject.Position.init), + shape: Geometry(location.getShape()), + edges: .init(uniqueKeysWithValues: edges) + ) + case .routeAlertLocation: + let routeAlertLocation = native.getRouteAlert() + self = .routeAlert(shape: Geometry(routeAlertLocation.getShape())) + @unknown default: + preconditionFailure("RoadObjectLocation can't be constructed. Unknown type.") + } + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcher.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcher.swift new file mode 100644 index 000000000..4bd93a3ef --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcher.swift @@ -0,0 +1,215 @@ +import Foundation +import MapboxCommon_Private +import MapboxNavigationNative +import MapboxNavigationNative_Private +import Turf + +/// Provides methods for road object matching. +/// +/// Matching results are delivered asynchronously via a delegate. +/// In case of error (if there are no tiles in the cache, decoding failed, etc.) the object won't be matched. +/// Use the ``RoadMatching/roadObjectMatcher`` from ``ElectronicHorizonController/roadMatching`` to access the +/// currently active road object matcher. +/// +/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to +/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms +/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require +/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the +/// feature. +public final class RoadObjectMatcher: @unchecked Sendable { + // MARK: Matching Objects + + /// Matches given OpenLR object to the graph. + /// - Parameters: + /// - location: OpenLR location of the road object, encoded in a base64 string. + /// - identifier: Unique identifier of the object. + public func matchOpenLR(location: String, identifier: OpenLRIdentifier) { + let standard = MapboxNavigationNative.Standard(identifier: identifier) + let reference: RoadObject.Identifier = switch identifier { + case .tomTom(let ref): + ref + case .tpeg(let ref): + ref + } + let openLR = MatchableOpenLr( + openlr: OpenLR(base64Encoded: location, standard: standard), + id: reference + ) + native.matchOpenLRs(for: [openLR], options: MatchingOptions( + useOnlyPreloadedTiles: false, + allowPartialMatching: false, + partialPolylineDistanceCalculationStrategy: .onlyMatched + )) + } + + /// Matches given polyline to the graph. + /// Polyline should define a valid path on the graph, i.e. it should be possible to drive this path according to + /// traffic rules. + /// - Parameters: + /// - polyline: Polyline representing the object. + /// - identifier: Unique identifier of the object. + public func match(polyline: LineString, identifier: RoadObject.Identifier) { + let polyline = MatchableGeometry(id: identifier, coordinates: polyline.coordinates.map(Coordinate2D.init)) + native.matchPolylines( + forPolylines: [polyline], + options: MatchingOptions( + useOnlyPreloadedTiles: false, + allowPartialMatching: false, + partialPolylineDistanceCalculationStrategy: .onlyMatched + ) + ) + } + + /// Matches a given polygon to the graph. + /// "Matching" here means we try to find all intersections of the polygon with the road graph and track distances to + /// those intersections as distance to the polygon. + /// - Parameters: + /// - polygon: Polygon representing the object. + /// - identifier: Unique identifier of the object. + public func match(polygon: Polygon, identifier: RoadObject.Identifier) { + let polygone = MatchableGeometry( + id: identifier, + coordinates: polygon.outerRing.coordinates.map(Coordinate2D.init) + ) + native.matchPolygons(forPolygons: [polygone], options: MatchingOptions( + useOnlyPreloadedTiles: false, + allowPartialMatching: false, + partialPolylineDistanceCalculationStrategy: .onlyMatched + )) + } + + /// Matches given gantry (i.e. polyline orthogonal to the road) to the graph. + /// "Matching" here means we try to find all intersections of the gantry with the road graph and track distances to + /// those intersections as distance to the gantry. + /// - Parameters: + /// - gantry: Gantry representing the object. + /// - identifier: Unique identifier of the object. + public func match(gantry: MultiPoint, identifier: RoadObject.Identifier) { + let gantry = MatchableGeometry(id: identifier, coordinates: gantry.coordinates.map(Coordinate2D.init)) + native.matchGantries( + forGantries: [gantry], + options: MatchingOptions( + useOnlyPreloadedTiles: false, + allowPartialMatching: false, + partialPolylineDistanceCalculationStrategy: .onlyMatched + ) + ) + } + + /// Matches given point to road graph. + /// - Parameters: + /// - point: Point representing the object. + /// - identifier: Unique identifier of the object. + /// - heading: Heading of the provided point, which is going to be matched. + public func match(point: CLLocationCoordinate2D, identifier: RoadObject.Identifier, heading: CLHeading? = nil) { + var trueHeading: NSNumber? + if let heading, heading.trueHeading >= 0.0 { + trueHeading = NSNumber(value: heading.trueHeading) + } + + let matchablePoint = MatchablePoint(id: identifier, coordinate: point, heading: trueHeading) + native.matchPoints( + for: [matchablePoint], + options: MatchingOptions( + useOnlyPreloadedTiles: false, + allowPartialMatching: false, + partialPolylineDistanceCalculationStrategy: .onlyMatched + ) + ) + } + + /// Cancel road object matching. + /// - Parameter identifier: Identifier for which matching should be canceled. + public func cancel(identifier: RoadObject.Identifier) { + native.cancel(forIds: [identifier]) + } + + // MARK: Observing Matching Results + + /// Road object matcher delegate. + public weak var delegate: RoadObjectMatcherDelegate? { + didSet { + if delegate != nil { + internalRoadObjectMatcherListener.delegate = delegate + } else { + internalRoadObjectMatcherListener.delegate = nil + } + updateListener() + } + } + + private func updateListener() { + if delegate != nil { + native.setListenerFor(internalRoadObjectMatcherListener) + } else { + native.setListenerFor(nil) + } + } + + var native: MapboxNavigationNative.RoadObjectMatcher { + didSet { + updateListener() + } + } + + /// Object, which subscribes to events being sent from the ``RoadObjectMatcherListener``, and passes them to the + /// ``RoadObjectMatcherDelegate``. + var internalRoadObjectMatcherListener: InternalRoadObjectMatcherListener! + + init(_ native: MapboxNavigationNative.RoadObjectMatcher) { + self.native = native + + self.internalRoadObjectMatcherListener = InternalRoadObjectMatcherListener(roadObjectMatcher: self) + } + + deinit { + internalRoadObjectMatcherListener.delegate = nil + native.setListenerFor(nil) + } +} + +extension MapboxNavigationNative.RoadObjectMatcherError: Error, @unchecked Sendable {} + +extension CLLocation { + convenience init(coordinate: CLLocationCoordinate2D) { + self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) + } +} + +// Since `MBXExpected` cannot be exposed publicly `InternalRoadObjectMatcherListener` works as an +// intermediary by subscribing to the events from the `RoadObjectMatcherListener`, and passing them +// to the `RoadObjectMatcherDelegate`. +class InternalRoadObjectMatcherListener: RoadObjectMatcherListener { + weak var roadObjectMatcher: RoadObjectMatcher? + + weak var delegate: RoadObjectMatcherDelegate? + + init(roadObjectMatcher: RoadObjectMatcher) { + self.roadObjectMatcher = roadObjectMatcher + } + + public func onRoadObjectMatched( + forRoadObject roadObject: Expected< + MapboxNavigationNative.RoadObject, + MapboxNavigationNative.RoadObjectMatcherError + > + ) { + guard let roadObjectMatcher else { return } + + let result = Result< + MapboxNavigationNative.RoadObject, + MapboxNavigationNative.RoadObjectMatcherError + >(expected: roadObject) + switch result { + case .success(let roadObject): + delegate?.roadObjectMatcher(roadObjectMatcher, didMatch: RoadObject(roadObject)) + case .failure(let error): + delegate?.roadObjectMatcher(roadObjectMatcher, didFailToMatchWith: RoadObjectMatcherError(error)) + } + } + + func onMatchingCancelled(forId id: String) { + guard let roadObjectMatcher else { return } + delegate?.roadObjectMatcher(roadObjectMatcher, didCancelMatchingFor: id) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherDelegate.swift new file mode 100644 index 000000000..787b2373a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherDelegate.swift @@ -0,0 +1,29 @@ +import Foundation +import MapboxNavigationNative + +/// ``RoadObjectMatcher`` delegate. +/// +/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to +/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms +/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require +/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the +/// feature. +public protocol RoadObjectMatcherDelegate: AnyObject { + /// This method is called with a road object when the matching is successfully finished. + /// - Parameters: + /// - matcher: The ``RoadObjectMatcher`` instance. + /// - roadObject: The matched ``RoadObject`` instance. + func roadObjectMatcher(_ matcher: RoadObjectMatcher, didMatch roadObject: RoadObject) + + /// This method is called when the matching is finished with error. + /// - Parameters: + /// - matcher: The ``RoadObjectMatcher`` instance. + /// - error: The ``RoadObjectMatcherError`` occured. + func roadObjectMatcher(_ matcher: RoadObjectMatcher, didFailToMatchWith error: RoadObjectMatcherError) + + /// This method is called when the matching is canceled. + /// - Parameters: + /// - matcher: The ``RoadObjectMatcher`` instance. + /// - id: The id of the ``RoadObject``. + func roadObjectMatcher(_ matcher: RoadObjectMatcher, didCancelMatchingFor id: String) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherError.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherError.swift new file mode 100644 index 000000000..453b37fa4 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherError.swift @@ -0,0 +1,36 @@ +import Foundation +import MapboxNavigationNative + +/// An error that occures during road object matching. +/// +/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to +/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms +/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require +/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the +/// feature. +public struct RoadObjectMatcherError: LocalizedError { + /// Description of the error. + public let description: String + + /// Identifier of the road object for which matching is failed. + public let roadObjectIdentifier: RoadObject.Identifier + + /// Description of the error. + public var errorDescription: String? { + return description + } + + /// Initializes a new ``RoadObjectMatcherError``. + /// - Parameters: + /// - description: Description of the error. + /// - roadObjectIdentifier: Identifier of the road object for which matching is failed. + public init(description: String, roadObjectIdentifier: RoadObject.Identifier) { + self.description = description + self.roadObjectIdentifier = roadObjectIdentifier + } + + init(_ native: MapboxNavigationNative.RoadObjectMatcherError) { + self.description = native.description + self.roadObjectIdentifier = native.roadObjectId + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectPosition.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectPosition.swift new file mode 100644 index 000000000..e26b77348 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectPosition.swift @@ -0,0 +1,29 @@ +import CoreLocation +import Foundation +import MapboxNavigationNative + +extension RoadObject { + /// Contains information about position of the point on the graph and it's geo-position. + public struct Position: Equatable, Sendable { + /// Position on the graph. + public let position: RoadGraph.Position + + /// Geo-position of the object. + public let coordinate: CLLocationCoordinate2D + + /// nitializes a new ``RoadObject/Position`` object with a given position on the graph and coordinate of the + /// object. + /// - Parameters: + /// - position: The position on the graph. + /// - coordinate: The location of the object. + public init(position: RoadGraph.Position, coordinate: CLLocationCoordinate2D) { + self.position = position + self.coordinate = coordinate + } + + init(_ native: MapboxNavigationNative.Position) { + self.position = RoadGraph.Position(native.position) + self.coordinate = native.coordinate + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStore.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStore.swift new file mode 100644 index 000000000..6f6a05ed6 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStore.swift @@ -0,0 +1,126 @@ +import Foundation +import MapboxNavigationNative + +extension RoadObject { + /// Identifies a road object in an electronic horizon. A road object represents a notable transition point along a + /// road, such as a toll booth or tunnel entrance. A road object is similar to a ``RouteAlert`` but is more closely + /// associated with the routing graph managed by the ``RoadGraph`` class. + /// + /// Use a ``RoadObjectStore`` object to get more information about a road object with a given identifier or get the + /// locations of road objects along ``RoadGraph/Edge``s. + public typealias Identifier = String +} + +/// Stores and provides access to metadata about road objects. +/// +/// You do not create a ``RoadObjectStore`` object manually. Instead, use the ``RoadMatching/roadObjectStore`` from +/// ``ElectronicHorizonController/roadMatching`` to access the currently active road object store. +/// +/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to +/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms +/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require +/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the +/// feature. +public final class RoadObjectStore: @unchecked Sendable { + /// The road object store’s delegate. + public weak var delegate: RoadObjectStoreDelegate? { + didSet { + updateObserver() + } + } + + // MARK: Getting Road Objects Data + + /// - Parameter edgeIdentifier: The identifier of the edge to query. + /// - Returns: Returns mapping `road object identifier ->` ``RoadObject/EdgeLocation`` for all road objects which + /// are lying on the edge with given identifier. + public func roadObjectEdgeLocations( + edgeIdentifier: RoadGraph.Edge + .Identifier + ) -> [RoadObject.Identifier: RoadObject.EdgeLocation] { + let objects = native.getForEdgeId(UInt64(edgeIdentifier)) + return objects.mapValues(RoadObject.EdgeLocation.init) + } + + /// Since road objects can be removed/added in background we should always check return value for `nil`, even if we + /// know that we have object with such identifier based on previous calls. + /// - Parameter roadObjectIdentifier: The identifier of the road object to query. + /// - Returns: Road object with given identifier, if such object cannot be found returns `nil`. + public func roadObject(identifier roadObjectIdentifier: RoadObject.Identifier) -> RoadObject? { + if let roadObject = native.getRoadObject(forId: roadObjectIdentifier) { + return RoadObject(roadObject) + } + return nil + } + + /// Returns the list of road object ids which are (partially) belong to `edgeIds`. + /// - Parameter edgeIdentifiers: The list of edge ids. + /// - Returns: The list of road object ids which are (partially) belong to `edgeIds`. + public func roadObjectIdentifiers(edgeIdentifiers: [RoadGraph.Edge.Identifier]) -> [RoadObject.Identifier] { + return native.getRoadObjectIdsByEdgeIds(forEdgeIds: edgeIdentifiers.map(NSNumber.init)) + } + + // MARK: Managing Custom Road Objects + + /// Adds a road object to be tracked in the electronic horizon. In case if an object with such identifier already + /// exists, updates it. + /// - Note: a road object obtained from route alerts cannot be added via this API. + /// - Parameter roadObject: Custom road object, acquired from ``RoadObjectMatcher``. + public func addUserDefinedRoadObject(_ roadObject: RoadObject) { + guard let nativeObject = roadObject.native else { + preconditionFailure("You can only add matched a custom road object, acquired from RoadObjectMatcher.") + } + native.addCustomRoadObject(for: nativeObject) + } + + /// Removes road object and stops tracking it in the electronic horizon. + /// - Parameter identifier: Identifier of the road object that should be removed. + public func removeUserDefinedRoadObject(identifier: RoadObject.Identifier) { + native.removeCustomRoadObject(forId: identifier) + } + + /// Removes all user-defined road objects from the store and stops tracking them in the electronic horizon. + public func removeAllUserDefinedRoadObjects() { + native.removeAllCustomRoadObjects() + } + + init(_ native: MapboxNavigationNative.RoadObjectsStore) { + self.native = native + } + + deinit { + if native.hasObservers() { + native.removeObserver(for: self) + } + } + + var native: MapboxNavigationNative.RoadObjectsStore { + didSet { + updateObserver() + } + } + + private func updateObserver() { + if delegate != nil { + native.addObserver(for: self) + } else { + if native.hasObservers() { + native.removeObserver(for: self) + } + } + } +} + +extension RoadObjectStore: RoadObjectsStoreObserver { + public func onRoadObjectAdded(forId id: String) { + delegate?.didAddRoadObject(identifier: id) + } + + public func onRoadObjectUpdated(forId id: String) { + delegate?.didUpdateRoadObject(identifier: id) + } + + public func onRoadObjectRemoved(forId id: String) { + delegate?.didRemoveRoadObject(identifier: id) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStoreDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStoreDelegate.swift new file mode 100644 index 000000000..a8f0efaf6 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStoreDelegate.swift @@ -0,0 +1,21 @@ +import Foundation + +/// ``RoadObjectStore`` delegate. +/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to +/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms +/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require +/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the +/// feature. +public protocol RoadObjectStoreDelegate: AnyObject { + /// This method is called when a road object with the given identifier has been added to the road objects store. + /// - Parameter identifier: The identifier of the road object. + func didAddRoadObject(identifier: RoadObject.Identifier) + + /// This method is called when a road object with the given identifier has been updated in the road objects store. + /// - Parameter identifier: The identifier of the road object. + func didUpdateRoadObject(identifier: RoadObject.Identifier) + + /// This method is called when a road object with the given identifier has been removed from the road objects store. + /// - Parameter identifier: The identifier of the road object. + func didRemoveRoadObject(identifier: RoadObject.Identifier) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadShield.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadShield.swift new file mode 100644 index 000000000..b09072d87 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadShield.swift @@ -0,0 +1,42 @@ +import MapboxNavigationNative + +/// Describes a road shield information. +/// +/// - note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta +/// and is subject to changes, including its pricing. Use of the feature is subject to the beta product restrictions +/// in the Mapbox Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at +/// any time and require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of +/// the level of use of the feature. +public struct RoadShield: Equatable, Sendable { + /// The base url for a shield image. + public let baseUrl: String + + /// The shield display reference. + public let displayRef: String + + /// The shield text. + public let name: String + + /// The string indicating the color of the text to be rendered on the route shield, e.g. "black". + public let textColor: String + + /// Creates a new `Shield` instance. + /// - Parameters: + /// - baseUrl: The base url for a shield image. + /// - displayRef: The shield display reference. + /// - name: The shield text. + /// - textColor: The string indicating the color of the text to be rendered on the route shield. + public init(baseUrl: String, displayRef: String, name: String, textColor: String) { + self.baseUrl = baseUrl + self.displayRef = displayRef + self.name = name + self.textColor = textColor + } + + init(_ native: MapboxNavigationNative.Shield) { + self.baseUrl = native.baseUrl + self.name = native.name + self.displayRef = native.displayRef + self.textColor = native.textColor + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadSubgraphEdge.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadSubgraphEdge.swift new file mode 100644 index 000000000..9ee68aa8f --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadSubgraphEdge.swift @@ -0,0 +1,65 @@ +import Foundation +import MapboxNavigationNative +import Turf + +extension RoadGraph { + /// The ``RoadGraph/SubgraphEdge`` represents an edge in the complex object which might be considered as a directed + /// graph. The graph might contain loops. ``innerEdgeIds`` and ``outerEdgeIds`` properties contain edge ids, which + /// allows to traverse the graph, obtain geometry and calculate different distances inside it. + /// + /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to + /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox + /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and + /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level + /// of use of the feature. + public struct SubgraphEdge: Equatable, Sendable { + /// Unique identifier of an edge. + /// + /// Use a ``RoadGraph`` object to get more information about the edge with a given identifier. + public typealias Identifier = Edge.Identifier + + /// Unique identifier of the edge. + public let identifier: Identifier + + /// The identifiers of edges in the subgraph from which the user could transition to this edge. + public let innerEdgeIds: [Identifier] + + /// The identifiers of edges in the subgraph to which the user could transition from this edge. + public let outerEdgeIds: [Identifier] + + /// The length of the edge mesured in meters. + public let length: CLLocationDistance + + /// The edge shape geometry. + public let shape: Turf.Geometry + + /// Initializes a new ``RoadGraph/SubgraphEdge`` object. + /// - Parameters: + /// - identifier: The unique identifier of an edge. + /// - innerEdgeIds: The edges from which the user could transition to this edge. + /// - outerEdgeIds: The edges to which the user could transition from this edge. + /// - length: The length of the edge mesured in meters. + /// - shape: The edge shape geometry. + public init( + identifier: Identifier, + innerEdgeIds: [Identifier], + outerEdgeIds: [Identifier], + length: CLLocationDistance, + shape: Turf.Geometry + ) { + self.identifier = identifier + self.innerEdgeIds = innerEdgeIds + self.outerEdgeIds = outerEdgeIds + self.length = length + self.shape = shape + } + + init(_ native: MapboxNavigationNative.SubgraphEdge) { + self.identifier = UInt(native.id) + self.innerEdgeIds = native.innerEdgeIds.map(UInt.init(truncating:)) + self.outerEdgeIds = native.outerEdgeIds.map(UInt.init(truncating:)) + self.length = native.length + self.shape = Turf.Geometry(native.shape) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RouteAlert.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RouteAlert.swift new file mode 100644 index 000000000..705f23620 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RouteAlert.swift @@ -0,0 +1,22 @@ +import CoreLocation +import Foundation +import MapboxDirections +import MapboxNavigationNative + +/// ``RouteAlert`` encapsulates information about various incoming events. Common attributes like location, distance to +/// the event, length and other is provided for each POI, while specific meta data is supplied via ``roadObject`` +/// property. +public struct RouteAlert: Equatable, Sendable { + /// Road object which describes upcoming route alert. + public let roadObject: RoadObject + + /// Distance from current position to alert, meters. + /// + /// This value can be negative if it is a spanned alert and we are somewhere in the middle of it. + public let distanceToStart: CLLocationDistance + + init(_ native: UpcomingRouteAlert, distanceToStart: CLLocationDistance) { + self.roadObject = RoadObject(native.roadObject) + self.distanceToStart = distanceToStart + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EtaDistanceInfo.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EtaDistanceInfo.swift new file mode 100644 index 000000000..9abe2724d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EtaDistanceInfo.swift @@ -0,0 +1,12 @@ +import CoreLocation +import Foundation + +public struct EtaDistanceInfo: Equatable, Sendable { + public var distance: CLLocationDistance + public var travelTime: TimeInterval? + + public init(distance: CLLocationDistance, travelTime: TimeInterval?) { + self.distance = distance + self.travelTime = travelTime + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/FasterRouteController.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/FasterRouteController.swift new file mode 100644 index 000000000..8a68b3fe2 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/FasterRouteController.swift @@ -0,0 +1,152 @@ +import Combine +import CoreLocation +import Foundation +import MapboxDirections + +public protocol FasterRouteProvider: AnyObject { + var isRerouting: Bool { get set } + var navigationRoute: NavigationRoute? { get set } + var currentLocation: CLLocation? { get set } + var fasterRoutes: AnyPublisher { get } + + func checkForFasterRoute( + from routeProgress: RouteProgress + ) +} + +final class FasterRouteController: FasterRouteProvider, @unchecked Sendable { + struct Configuration { + let settings: FasterRouteDetectionConfig + let initialManeuverAvoidanceRadius: TimeInterval + let routingProvider: RoutingProvider + } + + let configuration: Configuration + + private var lastProactiveRerouteDate: Date? + private var routeTask: RoutingProvider.FetchTask? + + var isRerouting: Bool + var navigationRoute: NavigationRoute? + var currentLocation: CLLocation? + + private var _fasterRoutes: PassthroughSubject + var fasterRoutes: AnyPublisher { + _fasterRoutes.eraseToAnyPublisher() + } + + init(configuration: Configuration) { + self.configuration = configuration + + self.lastProactiveRerouteDate = nil + self.isRerouting = false + self.routeTask = nil + self._fasterRoutes = .init() + } + + func checkForFasterRoute( + from routeProgress: RouteProgress + ) { + Task { + guard let routeOptions = navigationRoute?.routeOptions, + let location = currentLocation else { return } + + // Only check for faster alternatives if the user has plenty of time left on the route. + guard routeProgress.durationRemaining > configuration.settings.minimumRouteDurationRemaining else { return } + // If the user is approaching a maneuver, don't check for a faster alternatives + guard routeProgress.currentLegProgress.currentStepProgress.durationRemaining > configuration.settings + .minimumManeuverOffset else { return } + + guard let currentUpcomingManeuver = routeProgress.currentLegProgress.upcomingStep else { + return + } + + guard let lastRouteValidationDate = lastProactiveRerouteDate else { + self.lastProactiveRerouteDate = location.timestamp + return + } + + // Only check every so often for a faster route. + guard location.timestamp.timeIntervalSince(lastRouteValidationDate) >= configuration.settings + .proactiveReroutingInterval + else { + return + } + + let durationRemaining = routeProgress.durationRemaining + + // Avoid interrupting an ongoing reroute + if isRerouting { return } + isRerouting = true + + defer { self.isRerouting = false } + + guard let navigationRoutes = await calculateRoutes( + from: location, + along: routeProgress, + options: routeOptions + ) else { + return + } + let route = navigationRoutes.mainRoute.route + + self.lastProactiveRerouteDate = nil + + guard let firstLeg = route.legs.first, let firstStep = firstLeg.steps.first else { + return + } + + let routeIsFaster = firstStep.expectedTravelTime >= self.configuration.settings.minimumManeuverOffset && + currentUpcomingManeuver == firstLeg.steps[1] && route.expectedTravelTime <= 0.9 * durationRemaining + + guard routeIsFaster else { + return + } + + let completion = { @MainActor in + self._fasterRoutes.send(navigationRoutes) + } + + switch self.configuration.settings.fasterRouteApproval { + case .automatically: + await completion() + case .manually(let approval): + if await approval((location, navigationRoutes.mainRoute)) { + await completion() + } + } + } + } + + private func calculateRoutes( + from origin: CLLocation, + along progress: RouteProgress, + options: RouteOptions + ) async -> NavigationRoutes? { + routeTask?.cancel() + + let options = progress.reroutingOptions(from: origin, routeOptions: options) + + // https://github.com/mapbox/mapbox-navigation-ios/issues/3966 + if isRerouting, + options.profileIdentifier == .automobile || options.profileIdentifier == .automobileAvoidingTraffic + { + options.initialManeuverAvoidanceRadius = configuration.initialManeuverAvoidanceRadius * origin.speed + } + + let task = configuration.routingProvider.calculateRoutes(options: options) + routeTask = task + defer { self.routeTask = nil } + + do { + let routes = try await task.value + return await routes.selectingMostSimilar(to: progress.route) + } catch { + Log.warning( + "Failed to fetch proactive reroute with error: \(error)", + category: .navigation + ) + return nil + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/CoreNavigator.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/CoreNavigator.swift new file mode 100644 index 000000000..cc474d751 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/CoreNavigator.swift @@ -0,0 +1,518 @@ +import CoreLocation +import MapboxCommon_Private +import MapboxDirections +import MapboxNavigationNative +import UIKit + +protocol CoreNavigator { + var rawLocation: CLLocation? { get } + var mostRecentNavigationStatus: NavigationStatus? { get } + var tileStore: TileStore { get } + var roadGraph: RoadGraph { get } + var roadObjectStore: RoadObjectStore { get } + var roadObjectMatcher: RoadObjectMatcher { get } + var rerouteController: RerouteController? { get } + + @MainActor + func startUpdatingElectronicHorizon(with options: ElectronicHorizonConfig?) + @MainActor + func stopUpdatingElectronicHorizon() + + @MainActor + func setRoutes( + _ routesData: RoutesData, + uuid: UUID, + legIndex: UInt32, + reason: SetRoutesReason, + completion: @escaping @Sendable (Result) -> Void + ) + @MainActor + func setAlternativeRoutes( + with routes: [RouteInterface], + completion: @escaping @Sendable (Result<[RouteAlternative], Error>) -> Void + ) + @MainActor + func updateRouteLeg(to index: UInt32, completion: @escaping @Sendable (Bool) -> Void) + @MainActor + func unsetRoutes( + uuid: UUID, + completion: @escaping @Sendable (Result) -> Void + ) + + @MainActor + func updateLocation(_ location: CLLocation, completion: @escaping @Sendable (Bool) -> Void) + + @MainActor + func resume() + @MainActor + func pause() +} + +final class NativeNavigator: CoreNavigator, @unchecked Sendable { + struct Configuration: @unchecked Sendable { + let credentials: ApiConfiguration + let nativeHandlersFactory: NativeHandlersFactory + let routingConfig: RoutingConfig + let predictiveCacheManager: PredictiveCacheManager? + } + + let configuration: Configuration + + private(set) var navigator: NavigationNativeNavigator + private(set) var telemetrySessionManager: NavigationSessionManager + + private(set) var cacheHandle: CacheHandle + + var mostRecentNavigationStatus: NavigationStatus? { + navigatorStatusObserver?.mostRecentNavigationStatus + } + + private(set) var rawLocation: CLLocation? + + private(set) var tileStore: TileStore + + // Current Navigator status in terms of tile versioning. + var tileVersionState: NavigatorFallbackVersionsObserver.TileVersionState { + navigatorFallbackVersionsObserver?.tileVersionState ?? .nominal + } + + @MainActor + private lazy var routeCoordinator: RoutesCoordinator = .init( + routesSetupHandler: { @MainActor [weak self] routesData, legIndex, reason, completion in + + let dataParams = routesData.map { SetRoutesDataParams( + routes: $0, + legIndex: legIndex + ) } + + self?.navigator.native.setRoutesDataFor( + dataParams, + reason: reason + ) { [weak self] result in + if result.isValue(), + let routesResult = result.value + { + Log.info( + "Navigator has been updated, including \(routesResult.alternatives.count) alternatives.", + category: .navigation + ) + completion(.success((routesData?.primaryRoute().getRouteInfo(), routesResult.alternatives))) + } else if result.isError() { + let reason = (result.error as String?) ?? "" + Log.error("Failed to update navigator with reason: \(reason)", category: .navigation) + completion(.failure(NativeNavigatorError.failedToUpdateRoutes(reason: reason))) + } else { + assertionFailure("Invalid Expected value: \(result)") + completion(.failure( + NativeNavigatorError + .failedToUpdateRoutes(reason: "Unexpected internal response") + )) + } + } + }, + alternativeRoutesSetupHandler: { @MainActor [weak self] routes, completion in + self?.navigator.native.setAlternativeRoutesForRoutes(routes, callback: { result in + if result.isValue(), + let alternatives = result.value as? [RouteAlternative] + { + Log.info( + "Navigator alternative routes has been updated (\(alternatives.count) alternatives set).", + category: .navigation + ) + completion(.success(alternatives)) + } else { + let reason = (result.error as String?) ?? "" + Log.error( + "Failed to update navigator alternative routes with reason: \(reason)", + category: .navigation + ) + completion(.failure(NativeNavigatorError.failedToUpdateAlternativeRoutes(reason: reason))) + } + }) + } + ) + + private(set) var rerouteController: RerouteController? + + @MainActor + init(with configuration: Configuration) { + self.configuration = configuration + + let factory = configuration.nativeHandlersFactory + self.tileStore = factory.tileStore + self.cacheHandle = factory.cacheHandle + self.roadGraph = factory.roadGraph + self.navigator = factory.navigator + self.telemetrySessionManager = NavigationSessionManagerImp(navigator: navigator) + self.roadObjectStore = RoadObjectStore(navigator.native.roadObjectStore()) + self.roadObjectMatcher = RoadObjectMatcher(MapboxNavigationNative.RoadObjectMatcher(cache: cacheHandle)) + self.rerouteController = RerouteController( + configuration: .init( + credentials: configuration.credentials, + navigator: navigator, + configHandle: factory.configHandle(), + rerouteConfig: configuration.routingConfig.rerouteConfig, + initialManeuverAvoidanceRadius: configuration.routingConfig.initialManeuverAvoidanceRadius + ) + ) + + subscribeNavigator() + setupAlternativesControllerIfNeeded() + setupPredictiveCacheIfNeeded() + subscribeToNotifications() + } + + /// Destroys and creates new instance of Navigator together with other related entities. + /// + /// Typically, this method is used to restart a Navigator with a specific Version during switching to offline or + /// online modes. + /// - Parameter version: String representing a tile version name. `nil` value means "latest". Specifying exact + /// version also enables `fallback` mode which will passively monitor newer version available and will notify + /// `tileVersionState` if found. + @MainActor + func restartNavigator(forcing version: String? = nil) { + let previousNavigationSessionState = navigator.native.storeNavigationSession() + let previousSession = telemetrySessionManager as? NavigationSessionManagerImp + unsubscribeNavigator() + navigator.native.shutdown() + + let factory = configuration.nativeHandlersFactory.targeting(version: version) + + tileStore = factory.tileStore + cacheHandle = factory.cacheHandle + roadGraph = factory.roadGraph + navigator = factory.navigator + + navigator.native.restoreNavigationSession(for: previousNavigationSessionState) + telemetrySessionManager = NavigationSessionManagerImp(navigator: navigator, previousSession: previousSession) + roadObjectStore.native = navigator.native.roadObjectStore() + roadObjectMatcher.native = MapboxNavigationNative.RoadObjectMatcher(cache: cacheHandle) + rerouteController = RerouteController( + configuration: .init( + credentials: configuration.credentials, + navigator: navigator, + configHandle: factory.configHandle(), + rerouteConfig: configuration.routingConfig.rerouteConfig, + initialManeuverAvoidanceRadius: configuration.routingConfig.initialManeuverAvoidanceRadius + ) + ) + + subscribeNavigator() + setupAlternativesControllerIfNeeded() + setupPredictiveCacheIfNeeded() + } + + // MARK: - Subscriptions + + private weak var navigatorStatusObserver: NavigatorStatusObserver? + private weak var navigatorFallbackVersionsObserver: NavigatorFallbackVersionsObserver? + private weak var navigatorElectronicHorizonObserver: NavigatorElectronicHorizonObserver? + private weak var navigatorAlternativesObserver: NavigatorRouteAlternativesObserver? + private weak var navigatorRouteRefreshObserver: NavigatorRouteRefreshObserver? + + private func setupPredictiveCacheIfNeeded() { + guard let predictiveCacheManager = configuration.predictiveCacheManager, + case .nominal = tileVersionState else { return } + + Task { @MainActor in + predictiveCacheManager.updateNavigationController(with: navigator) + predictiveCacheManager.updateSearchController(with: navigator) + } + } + + @MainActor + private func setupAlternativesControllerIfNeeded() { + guard let alternativeRoutesDetectionConfig = configuration.routingConfig.alternativeRoutesDetectionConfig + else { return } + + guard let refreshIntervalSeconds = UInt16(exactly: alternativeRoutesDetectionConfig.refreshIntervalSeconds) + else { + assertionFailure("'refreshIntervalSeconds' has an unexpected value.") + return + } + + let configManeuverAvoidanceRadius = configuration.routingConfig.initialManeuverAvoidanceRadius + guard let initialManeuverAvoidanceRadius = Float(exactly: configManeuverAvoidanceRadius) else { + assertionFailure("'initialManeuverAvoidanceRadius' has an unexpected value.") + return + } + + navigator.native.getRouteAlternativesController().setRouteAlternativesOptionsFor( + RouteAlternativesOptions( + requestIntervalSeconds: refreshIntervalSeconds, + minTimeBeforeManeuverSeconds: initialManeuverAvoidanceRadius + ) + ) + } + + @MainActor + fileprivate func subscribeContinuousAlternatives() { + if configuration.routingConfig.alternativeRoutesDetectionConfig != nil { + let alternativesObserver = NavigatorRouteAlternativesObserver() + navigatorAlternativesObserver = alternativesObserver + navigator.native.getRouteAlternativesController().addObserver(for: alternativesObserver) + } else if let navigatorAlternativesObserver { + navigator.native.getRouteAlternativesController().removeObserver(for: navigatorAlternativesObserver) + self.navigatorAlternativesObserver = nil + } + } + + @MainActor + fileprivate func subscribeFallbackObserver() { + let versionsObserver = NavigatorFallbackVersionsObserver(restartCallback: { [weak self] targetVersion in + if let self { + _Concurrency.Task { @MainActor in + self.restartNavigator(forcing: targetVersion) + } + } + }) + navigatorFallbackVersionsObserver = versionsObserver + navigator.native.setFallbackVersionsObserverFor(versionsObserver) + } + + @MainActor + fileprivate func subscribeStatusObserver() { + let statusObserver = NavigatorStatusObserver() + navigatorStatusObserver = statusObserver + navigator.native.addObserver(for: statusObserver) + } + + @MainActor + fileprivate func subscribeElectornicHorizon() { + guard isSubscribedToElectronicHorizon else { + return + } + startUpdatingElectronicHorizon( + with: electronicHorizonConfig, + on: navigator + ) + } + + @MainActor + fileprivate func subscribeRouteRefreshing() { + guard let refreshPeriod = configuration.routingConfig.routeRefreshPeriod else { + return + } + + let refreshObserver = + NavigatorRouteRefreshObserver(refreshCallback: { [weak self] refreshResponse, routeId, geometryIndex in + return await withCheckedContinuation { continuation in + self?.navigator.native.refreshRoute( + forRouteRefreshResponse: refreshResponse, + routeId: routeId, + geometryIndex: geometryIndex + ) { result in + _Concurrency.Task { + if result.isValue() { + continuation.resume( + returning: RouteRefreshResult( + updatedRoute: result.value.route, + alternativeRoutes: result.value.alternatives + ) + ) + } else if result.isError(), + let error = result.error + { + Log.warning( + "Failed to apply route refresh response with error: \(error)", + category: .navigation + ) + continuation.resume(returning: nil) + } + } + } + } + }) + navigator.native.addRouteRefreshObserver(for: refreshObserver) + navigator.native.startRoutesRefresh( + forDefaultRefreshPeriodMs: UInt64(refreshPeriod * 1000), + ignoreExpirationTime: true + ) + } + + @MainActor + private func subscribeNavigator() { + subscribeElectornicHorizon() + subscribeStatusObserver() + subscribeFallbackObserver() + subscribeContinuousAlternatives() + subscribeRouteRefreshing() + } + + fileprivate func unsubscribeRouteRefreshing() { + guard let navigatorRouteRefreshObserver else { + return + } + navigator.removeRouteRefreshObserver( + for: navigatorRouteRefreshObserver + ) + } + + fileprivate func unsubscribeContinuousAlternatives() { + guard let navigatorAlternativesObserver else { + return + } + navigator.removeRouteAlternativesObserver( + navigatorAlternativesObserver + ) + self.navigatorAlternativesObserver = nil + } + + fileprivate func unsubscribeFallbackObserver() { + navigator.setFallbackVersionsObserverFor( + nil + ) + } + + fileprivate func unsubscribeStatusObserver() { + if let navigatorStatusObserver { + navigator.removeObserver( + for: navigatorStatusObserver + ) + } + } + + private func unsubscribeNavigator() { + stopUpdatingElectronicHorizon(on: navigator) + unsubscribeStatusObserver() + unsubscribeFallbackObserver() + unsubscribeContinuousAlternatives() + unsubscribeRouteRefreshing() + } + + // MARK: - Electronic horizon + + private(set) var roadGraph: RoadGraph + + private(set) var roadObjectStore: RoadObjectStore + + private(set) var roadObjectMatcher: RoadObjectMatcher + + private var isSubscribedToElectronicHorizon = false + + private var electronicHorizonConfig: ElectronicHorizonConfig? { + didSet { + let nativeOptions = electronicHorizonConfig.map(MapboxNavigationNative.ElectronicHorizonOptions.init) + navigator.setElectronicHorizonOptionsFor( + nativeOptions + ) + } + } + + @MainActor + func startUpdatingElectronicHorizon(with config: ElectronicHorizonConfig?) { + startUpdatingElectronicHorizon(with: config, on: navigator) + } + + @MainActor + private func startUpdatingElectronicHorizon( + with config: ElectronicHorizonConfig?, + on navigator: NavigationNativeNavigator + ) { + isSubscribedToElectronicHorizon = true + + let observer = NavigatorElectronicHorizonObserver() + navigatorElectronicHorizonObserver = observer + navigator.native.setElectronicHorizonObserverFor(observer) + electronicHorizonConfig = config + } + + @MainActor + func stopUpdatingElectronicHorizon() { + stopUpdatingElectronicHorizon(on: navigator) + } + + private func stopUpdatingElectronicHorizon(on navigator: NavigationNativeNavigator) { + isSubscribedToElectronicHorizon = false + navigator.setElectronicHorizonObserverFor(nil) + electronicHorizonConfig = nil + } + + // MARK: - Navigator Updates + + @MainActor + func setRoutes( + _ routesData: RoutesData, + uuid: UUID, + legIndex: UInt32, + reason: SetRoutesReason, + completion: @escaping (Result) -> Void + ) { + routeCoordinator.beginActiveNavigation( + with: routesData, + uuid: uuid, + legIndex: legIndex, + reason: reason, + completion: completion + ) + } + + @MainActor + func setAlternativeRoutes( + with routes: [RouteInterface], + completion: @escaping (Result<[RouteAlternative], Error>) -> Void + ) { + routeCoordinator.updateAlternativeRoutes(with: routes, completion: completion) + } + + @MainActor + func updateRouteLeg(to index: UInt32, completion: @escaping (Bool) -> Void) { + let legIndex = UInt32(index) + + navigator.native.changeLeg(forLeg: legIndex, callback: completion) + } + + @MainActor + func unsetRoutes(uuid: UUID, completion: @escaping (Result) -> Void) { + routeCoordinator.endActiveNavigation(with: uuid, completion: completion) + } + + @MainActor + func updateLocation(_ rawLocation: CLLocation, completion: @escaping (Bool) -> Void) { + self.rawLocation = rawLocation + navigator.native.updateLocation(for: FixLocation(rawLocation), callback: completion) + } + + @MainActor + func pause() { + navigator.native.pause() + telemetrySessionManager.reportStopNavigation() + } + + @MainActor + func resume() { + navigator.native.resume() + telemetrySessionManager.reportStartNavigation() + } + + deinit { + unsubscribeNavigator() + if let predictiveCacheManager = configuration.predictiveCacheManager { + Task { @MainActor in + predictiveCacheManager.updateNavigationController(with: nil) + predictiveCacheManager.updateSearchController(with: nil) + } + } + } + + private func subscribeToNotifications() { + _Concurrency.Task { @MainActor in + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillTerminate), + name: UIApplication.willTerminateNotification, + object: nil + ) + } + } + + @objc + private func applicationWillTerminate(_ notification: NSNotification) { + telemetrySessionManager.reportStopNavigation() + } +} + +enum NativeNavigatorError: Swift.Error { + case failedToUpdateRoutes(reason: String) + case failedToUpdateAlternativeRoutes(reason: String) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/DefaultRerouteControllerInterface.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/DefaultRerouteControllerInterface.swift new file mode 100644 index 000000000..147a10ac4 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/DefaultRerouteControllerInterface.swift @@ -0,0 +1,29 @@ +import Foundation +import MapboxNavigationNative_Private + +class DefaultRerouteControllerInterface: RerouteControllerInterface { + typealias RequestConfiguration = (String) -> String + + let nativeInterface: RerouteControllerInterface? + let requestConfig: RequestConfiguration? + + init( + nativeInterface: RerouteControllerInterface?, + requestConfig: RequestConfiguration? = nil + ) { + self.nativeInterface = nativeInterface + self.requestConfig = requestConfig + } + + func reroute(forUrl url: String, callback: @escaping RerouteCallback) { + nativeInterface?.reroute(forUrl: requestConfig?(url) ?? url, callback: callback) + } + + func cancel() { + nativeInterface?.cancel() + } + + func setOptionsAdapterForRouteRequest(_ routeRequestOptionsAdapter: (any RouteOptionsAdapter)?) { + nativeInterface?.setOptionsAdapterForRouteRequest(routeRequestOptionsAdapter) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationNativeNavigator.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationNativeNavigator.swift new file mode 100644 index 000000000..26a4e07f0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationNativeNavigator.swift @@ -0,0 +1,143 @@ +import Combine +import Foundation +import MapboxCommon +@preconcurrency import MapboxNavigationNative +@preconcurrency import MapboxNavigationNative_Private + +final class NavigationNativeNavigator: @unchecked Sendable { + typealias Completion = @Sendable () -> Void + @MainActor + let native: MapboxNavigationNative.Navigator + var locale: Locale { + didSet { + Task { @MainActor in + updateLocale() + } + } + } + + private var subscriptions: Set = [] + + private func withNavigator(_ callback: @escaping @Sendable (MapboxNavigationNative.Navigator) -> Void) { + Task { @MainActor in + callback(native) + } + } + + @MainActor + init( + navigator: MapboxNavigationNative.Navigator, + locale: Locale + ) { + self.native = navigator + self.locale = locale + + NotificationCenter.default + .publisher(for: NSLocale.currentLocaleDidChangeNotification) + .sink { [weak self] _ in + self?.updateLocale() + } + .store(in: &subscriptions) + } + + @MainActor + private func updateLocale() { + native.config().mutableSettings().setUserLanguagesForLanguages(locale.preferredBCP47Codes) + } + + func removeRouteAlternativesObserver( + _ observer: RouteAlternativesObserver, + completion: Completion? = nil + ) { + withNavigator { + $0.getRouteAlternativesController().removeObserver(for: observer) + completion?() + } + } + + func startNavigationSession(completion: Completion? = nil) { + withNavigator { + $0.startNavigationSession() + completion?() + } + } + + func stopNavigationSession(completion: Completion? = nil) { + withNavigator { + $0.stopNavigationSession() + completion?() + } + } + + func setElectronicHorizonOptionsFor( + _ options: MapboxNavigationNative.ElectronicHorizonOptions?, + completion: Completion? = nil + ) { + withNavigator { + $0.setElectronicHorizonOptionsFor(options) + completion?() + } + } + + func setFallbackVersionsObserverFor( + _ observer: FallbackVersionsObserver?, + completion: Completion? = nil + ) { + withNavigator { + $0.setFallbackVersionsObserverFor(observer) + completion?() + } + } + + func removeObserver( + for observer: NavigatorObserver, + completion: Completion? = nil + ) { + withNavigator { + $0.removeObserver(for: observer) + completion?() + } + } + + func removeRouteRefreshObserver( + for observer: RouteRefreshObserver, + completion: Completion? = nil + ) { + withNavigator { + $0.removeRouteRefreshObserver(for: observer) + completion?() + } + } + + func setElectronicHorizonObserverFor( + _ observer: ElectronicHorizonObserver?, + completion: Completion? = nil + ) { + withNavigator { + $0.setElectronicHorizonObserverFor(observer) + completion?() + } + } + + func setRerouteControllerForController( + _ controller: RerouteControllerInterface, + completion: Completion? = nil + ) { + withNavigator { + $0.setRerouteControllerForController(controller) + completion?() + } + } + + func removeRerouteObserver( + for observer: RerouteObserver, + completion: Completion? = nil + ) { + withNavigator { + $0.removeRerouteObserver(for: observer) + completion?() + } + } +} + +extension MapboxNavigationNative.ElectronicHorizonOptions: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationSessionManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationSessionManager.swift new file mode 100644 index 000000000..fbf31b137 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationSessionManager.swift @@ -0,0 +1,42 @@ +import Foundation +import MapboxNavigationNative + +protocol NavigationSessionManager { + func reportStartNavigation() + func reportStopNavigation() +} + +final class NavigationSessionManagerImp: NavigationSessionManager { + private let lock: NSLock = .init() + + private var sessionCount: Int + + private let navigator: NavigationNativeNavigator + + init(navigator: NavigationNativeNavigator, previousSession: NavigationSessionManagerImp? = nil) { + self.navigator = navigator + self.sessionCount = previousSession?.sessionCount ?? 0 + } + + func reportStartNavigation() { + var shouldStart = false + lock { + shouldStart = sessionCount == 0 + sessionCount += 1 + } + if shouldStart { + navigator.startNavigationSession() + } + } + + func reportStopNavigation() { + var shouldStop = false + lock { + shouldStop = sessionCount == 1 + sessionCount = max(sessionCount - 1, 0) + } + if shouldStop { + navigator.stopNavigationSession() + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorElectronicHorizonObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorElectronicHorizonObserver.swift new file mode 100644 index 000000000..4885608d1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorElectronicHorizonObserver.swift @@ -0,0 +1,58 @@ +import Foundation +import MapboxNavigationNative + +class NavigatorElectronicHorizonObserver: ElectronicHorizonObserver { + public func onPositionUpdated( + for position: ElectronicHorizonPosition, + distances: [MapboxNavigationNative.RoadObjectDistance] + ) { + let positionInfo = RoadGraph.Position(position.position()) + let treeInfo = RoadGraph.Edge(position.tree().start) + let distancesInfo = distances.map(DistancedRoadObject.init) + let updatesMPP = position.type() == .update + + Task { @MainActor in + let userInfo: [RoadGraph.NotificationUserInfoKey: Any] = [ + .positionKey: positionInfo, + .treeKey: treeInfo, + .updatesMostProbablePathKey: updatesMPP, + .distancesByRoadObjectKey: distancesInfo, + ] + + NotificationCenter.default.post( + name: .electronicHorizonDidUpdatePosition, + object: nil, + userInfo: userInfo + ) + } + } + + public func onRoadObjectEnter(for info: RoadObjectEnterExitInfo) { + Task { @MainActor in + let userInfo: [RoadGraph.NotificationUserInfoKey: Any] = [ + .roadObjectIdentifierKey: info.roadObjectId, + .didTransitionAtEndpointKey: info.enterFromStartOrExitFromEnd, + ] + NotificationCenter.default.post(name: .electronicHorizonDidEnterRoadObject, object: nil, userInfo: userInfo) + } + } + + public func onRoadObjectExit(for info: RoadObjectEnterExitInfo) { + Task { @MainActor in + let userInfo: [RoadGraph.NotificationUserInfoKey: Any] = [ + .roadObjectIdentifierKey: info.roadObjectId, + .didTransitionAtEndpointKey: info.enterFromStartOrExitFromEnd, + ] + NotificationCenter.default.post(name: .electronicHorizonDidExitRoadObject, object: nil, userInfo: userInfo) + } + } + + public func onRoadObjectPassed(for info: RoadObjectPassInfo) { + Task { @MainActor in + let userInfo: [RoadGraph.NotificationUserInfoKey: Any] = [ + .roadObjectIdentifierKey: info.roadObjectId, + ] + NotificationCenter.default.post(name: .electronicHorizonDidPassRoadObject, object: nil, userInfo: userInfo) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorFallbackVersionsObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorFallbackVersionsObserver.swift new file mode 100644 index 000000000..2f816121c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorFallbackVersionsObserver.swift @@ -0,0 +1,66 @@ +import Foundation +import MapboxNavigationNative + +class NavigatorFallbackVersionsObserver: FallbackVersionsObserver { + private(set) var tileVersionState: TileVersionState = .nominal + + typealias RestartCallback = (String?) -> Void + let restartCallback: RestartCallback + + init(restartCallback: @escaping RestartCallback) { + self.restartCallback = restartCallback + } + + enum TileVersionState { + /// No tiles version switch is required. Navigator has enough tiles for map matching. + case nominal + /// Navigator does not have tiles on current version for map matching, but TileStore contains regions with + /// required tiles of a different version + case shouldFallback([String]) + /// Navigator is in a fallback mode but newer tiles version were successefully downloaded and ready to use. + case shouldReturnToLatest + } + + func onFallbackVersionsFound(forVersions versions: [String]) { + switch tileVersionState { + case .nominal, .shouldReturnToLatest: + tileVersionState = .shouldFallback(versions) + guard let fallbackVersion = versions.last else { return } + + restartCallback(fallbackVersion) + + let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ + .tilesVersionKey: fallbackVersion, + ] + + NotificationCenter.default.post( + name: .navigationDidSwitchToFallbackVersion, + object: nil, + userInfo: userInfo + ) + case .shouldFallback: + break // do nothing + } + } + + func onCanReturnToLatest(forVersion version: String) { + switch tileVersionState { + case .nominal, .shouldFallback: + tileVersionState = .shouldReturnToLatest + + restartCallback(nil) + + let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ + .tilesVersionKey: version, + ] + + NotificationCenter.default.post( + name: .navigationDidSwitchToTargetVersion, + object: nil, + userInfo: userInfo + ) + case .shouldReturnToLatest: + break // do nothing + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteAlternativesObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteAlternativesObserver.swift new file mode 100644 index 000000000..6f2963aff --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteAlternativesObserver.swift @@ -0,0 +1,42 @@ +import Foundation +import MapboxNavigationNative + +class NavigatorRouteAlternativesObserver: RouteAlternativesObserver { + func onRouteAlternativesUpdated( + forOnlinePrimaryRoute onlinePrimaryRoute: RouteInterface?, + alternatives: [RouteAlternative], + removedAlternatives: [RouteAlternative] + ) { + // do nothing + } + + func onRouteAlternativesChanged(for routeAlternatives: [RouteAlternative], removed: [RouteAlternative]) { + let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ + .alternativesListKey: routeAlternatives, + .removedAlternativesKey: removed, + ] + NotificationCenter.default.post(name: .navigatorDidChangeAlternativeRoutes, object: nil, userInfo: userInfo) + } + + public func onError(forMessage message: String) { + let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ + .messageKey: message, + ] + NotificationCenter.default.post( + name: .navigatorDidFailToChangeAlternativeRoutes, + object: nil, + userInfo: userInfo + ) + } + + func onOnlinePrimaryRouteAvailable(forOnlinePrimaryRoute onlinePrimaryRoute: RouteInterface) { + let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ + .coincideOnlineRouteKey: onlinePrimaryRoute, + ] + NotificationCenter.default.post( + name: .navigatorWantsSwitchToCoincideOnlineRoute, + object: nil, + userInfo: userInfo + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteRefreshObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteRefreshObserver.swift new file mode 100644 index 000000000..aa09e6f8d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteRefreshObserver.swift @@ -0,0 +1,67 @@ +import _MapboxNavigationHelpers +import Foundation +@preconcurrency import MapboxNavigationNative + +struct RouteRefreshResult: @unchecked Sendable { + let updatedRoute: RouteInterface + let alternativeRoutes: [RouteAlternative] +} + +class NavigatorRouteRefreshObserver: RouteRefreshObserver, @unchecked Sendable { + typealias RefreshCallback = (String, String, UInt32) async -> RouteRefreshResult? + private var refreshCallback: RefreshCallback + + init(refreshCallback: @escaping RefreshCallback) { + self.refreshCallback = refreshCallback + } + + func onRouteRefreshAnnotationsUpdated( + forRouteId routeId: String, + routeRefreshResponse: String, + routeIndex: UInt32, + legIndex: UInt32, + routeGeometryIndex: UInt32 + ) { + Task { + guard let routeRefreshResult = await self.refreshCallback( + routeRefreshResponse, + "\(routeId)#\(routeIndex)", + routeGeometryIndex + ) else { + return + } + let userInfo: [NativeNavigator.NotificationUserInfoKey: any Sendable] = [ + .refreshRequestIdKey: routeId, + .refreshedRoutesResultKey: routeRefreshResult, + .legIndexKey: legIndex, + ] + + onMainAsync { + NotificationCenter.default.post( + name: .routeRefreshDidUpdateAnnotations, + object: nil, + userInfo: userInfo + ) + } + } + } + + func onRouteRefreshCancelled(forRouteId routeId: String) { + let userInfo: [NativeNavigator.NotificationUserInfoKey: any Sendable] = [ + .refreshRequestIdKey: routeId, + ] + onMainAsync { + NotificationCenter.default.post(name: .routeRefreshDidCancelRefresh, object: nil, userInfo: userInfo) + } + } + + func onRouteRefreshFailed(forRouteId routeId: String, error: RouteRefreshError) { + let userInfo: [NativeNavigator.NotificationUserInfoKey: any Sendable] = [ + .refreshRequestErrorKey: error, + .refreshRequestIdKey: routeId, + ] + onMainAsync { + NotificationCenter.default.post(name: .routeRefreshDidFailRefresh, object: nil, userInfo: userInfo) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorStatusObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorStatusObserver.swift new file mode 100644 index 000000000..81dab8cb1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorStatusObserver.swift @@ -0,0 +1,18 @@ +import Foundation +import MapboxNavigationNative + +class NavigatorStatusObserver: NavigatorObserver { + var mostRecentNavigationStatus: NavigationStatus? = nil + + func onStatus(for origin: NavigationStatusOrigin, status: NavigationStatus) { + assert(Thread.isMainThread) + + let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ + .originKey: origin, + .statusKey: status, + ] + NotificationCenter.default.post(name: .navigationStatusDidChange, object: nil, userInfo: userInfo) + + mostRecentNavigationStatus = status + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/RerouteController.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/RerouteController.swift new file mode 100644 index 000000000..cfdde64f8 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/RerouteController.swift @@ -0,0 +1,151 @@ +import Foundation +import MapboxCommon +import MapboxDirections +import MapboxNavigationNative_Private + +/// Adapter for `MapboxNavigationNative.RerouteControllerInterface` usage inside `Navigator`. +/// +/// This class handles correct setup for `RerouteControllerInterface`, monitoring native reroute events and configuring +/// the process. +class RerouteController { + // MARK: Configuration + + struct Configuration { + let credentials: ApiConfiguration + let navigator: NavigationNativeNavigator + let configHandle: ConfigHandle + let rerouteConfig: RerouteConfig + let initialManeuverAvoidanceRadius: TimeInterval + } + + var initialManeuverAvoidanceRadius: TimeInterval { + get { + config.mutableSettings().avoidManeuverSeconds()?.doubleValue ?? defaultInitialManeuverAvoidanceRadius + } + set { + config.mutableSettings().setAvoidManeuverSecondsForSeconds(NSNumber(value: newValue)) + } + } + + private var config: ConfigHandle + private let rerouteConfig: RerouteConfig + private let defaultInitialManeuverAvoidanceRadius: TimeInterval + var abortReroutePipeline: Bool = false + + // MARK: Reporting Data + + weak var delegate: ReroutingControllerDelegate? + + func userIsOnRoute() -> Bool { + return !(rerouteDetector?.isReroute() ?? false) + } + + // MARK: Internal State Management + + private let defaultRerouteController: DefaultRerouteControllerInterface + private let rerouteDetector: RerouteDetectorInterface? + + private weak var navigator: NavigationNativeNavigator? + + @MainActor + required init(configuration: Configuration) { + self.rerouteConfig = configuration.rerouteConfig + self.navigator = configuration.navigator + self.config = configuration.configHandle + self.defaultInitialManeuverAvoidanceRadius = configuration.initialManeuverAvoidanceRadius + self.defaultRerouteController = DefaultRerouteControllerInterface( + nativeInterface: configuration.navigator.native.getRerouteController() + ) { + guard let url = URL(string: $0), + let options = RouteOptions(url: url) + else { + return $0 + } + + return Directions + .url( + forCalculating: configuration.rerouteConfig.optionsCustomization?(options) ?? options, + credentials: .init(configuration.credentials) + ) + .absoluteString + } + navigator?.native.setRerouteControllerForController(defaultRerouteController) + self.rerouteDetector = configuration.navigator.native.getRerouteDetector() + navigator?.native.addRerouteObserver(for: self) + + defer { + self.initialManeuverAvoidanceRadius = configuration.initialManeuverAvoidanceRadius + } + } + + deinit { + self.navigator?.removeRerouteObserver(for: self) + } +} + +extension RerouteController: RerouteObserver { + func onSwitchToAlternative(forRoute route: any RouteInterface, legIndex: UInt32) { + delegate?.rerouteControllerWantsSwitchToAlternative(self, route: route, legIndex: Int(legIndex)) + } + + func onRerouteDetected(forRouteRequest routeRequest: String) -> Bool { + guard rerouteConfig.detectsReroute else { return false } + delegate?.rerouteControllerDidDetectReroute(self) + return !abortReroutePipeline + } + + func onRerouteReceived(forRouteResponse routeResponse: DataRef, routeRequest: String, origin: RouterOrigin) { + guard rerouteConfig.detectsReroute else { + Log.warning( + "Reroute attempt fetched a route during 'rerouteConfig.detectsReroute' is disabled.", + category: .navigation + ) + return + } + + RouteParser.parseDirectionsResponse( + forResponseDataRef: routeResponse, + request: routeRequest, + routeOrigin: origin + ) { [weak self] result in + guard let self else { return } + + if result.isValue(), + var routes = result.value as? [RouteInterface], + !routes.isEmpty + { + let routesData = RouteParser.createRoutesData( + forPrimaryRoute: routes.remove(at: 0), + alternativeRoutes: routes + ) + delegate?.rerouteControllerDidRecieveReroute(self, routesData: routesData) + } else { + delegate?.rerouteControllerDidFailToReroute(self, with: DirectionsError.invalidResponse(nil)) + } + } + } + + func onRerouteCancelled() { + guard rerouteConfig.detectsReroute else { return } + delegate?.rerouteControllerDidCancelReroute(self) + } + + func onRerouteFailed(forError error: RerouteError) { + guard rerouteConfig.detectsReroute else { + Log.warning( + "Reroute attempt failed with an error during 'rerouteConfig.detectsReroute' is disabled. Error: \(error.message)", + category: .navigation + ) + return + } + delegate?.rerouteControllerDidFailToReroute( + self, + with: DirectionsError.unknown( + response: nil, + underlying: ReroutingError(error), + code: nil, + message: error.message + ) + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/ReroutingControllerDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/ReroutingControllerDelegate.swift new file mode 100644 index 000000000..6630d83fe --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/ReroutingControllerDelegate.swift @@ -0,0 +1,51 @@ +import MapboxDirections +import MapboxNavigationNative +import Turf + +protocol ReroutingControllerDelegate: AnyObject { + func rerouteControllerWantsSwitchToAlternative( + _ rerouteController: RerouteController, + route: RouteInterface, + legIndex: Int + ) + func rerouteControllerDidDetectReroute(_ rerouteController: RerouteController) + func rerouteControllerDidRecieveReroute(_ rerouteController: RerouteController, routesData: RoutesData) + func rerouteControllerDidCancelReroute(_ rerouteController: RerouteController) + func rerouteControllerDidFailToReroute(_ rerouteController: RerouteController, with error: DirectionsError) +} + +/// Error type, describing rerouting process malfunction. +public enum ReroutingError: Error { + /// Could not correctly process the reroute. + case routeError + /// Could not compose correct request for rerouting. + case wrongRequest + /// Cause of reroute error is unknown. + case unknown + /// Reroute was cancelled by user. + case cancelled + /// No routes or reroute controller was set to Navigator + case noRoutesOrController + /// Another reroute is in progress. + case anotherRerouteInProgress + + init?(_ nativeError: RerouteError) { + switch nativeError.type { + case .routerError: + self = .routeError + case .unknown: + self = .unknown + case .cancelled: + self = .cancelled + case .noRoutesOrController: + self = .noRoutesOrController + case .buildUriError: + self = .wrongRequest + case .rerouteInProgress: + self = .anotherRerouteInProgress + @unknown default: + assertionFailure("Unknown MapboxNavigationNative.RerouteError value.") + return nil + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/HandlerFactory.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/HandlerFactory.swift new file mode 100644 index 000000000..36435d047 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/HandlerFactory.swift @@ -0,0 +1,87 @@ +import MapboxDirections +import MapboxNavigationNative + +protocol HandlerData { + var tileStorePath: String { get } + var apiConfiguration: ApiConfiguration { get } + var tilesVersion: String { get } + var targetVersion: String? { get } + var configFactoryType: ConfigFactory.Type { get } + var datasetProfileIdentifier: ProfileIdentifier { get } +} + +extension NativeHandlersFactory: HandlerData {} + +/// Creates new or returns existing entity of `HandlerType` constructed with `Arguments`. +/// +/// This factory is required since some of NavNative's handlers are used by multiple unrelated entities and is quite +/// expensive to allocate. Since bindgen-generated `*Factory` classes are not an actual factory but just a wrapper +/// around general init, `HandlerFactory` introduces basic caching of the latest allocated entity. In most of the cases +/// there should never be multiple handlers with different attributes, so such solution is adequate at the moment. +class HandlerFactory { + private struct CacheKey: HandlerData { + let tileStorePath: String + let apiConfiguration: ApiConfiguration + let tilesVersion: String + let targetVersion: String? + let configFactoryType: ConfigFactory.Type + let datasetProfileIdentifier: ProfileIdentifier + + init(data: HandlerData) { + self.tileStorePath = data.tileStorePath + self.apiConfiguration = data.apiConfiguration + self.tilesVersion = data.tilesVersion + self.targetVersion = data.targetVersion + self.configFactoryType = data.configFactoryType + self.datasetProfileIdentifier = data.datasetProfileIdentifier + } + + static func != (lhs: CacheKey, rhs: HandlerData) -> Bool { + return lhs.tileStorePath != rhs.tileStorePath || + lhs.apiConfiguration != rhs.apiConfiguration || + lhs.tilesVersion != rhs.tilesVersion || + lhs.targetVersion != rhs.targetVersion || + lhs.configFactoryType != rhs.configFactoryType || + lhs.datasetProfileIdentifier != rhs.datasetProfileIdentifier + } + } + + typealias BuildHandler = (Arguments) -> HandlerType + let buildHandler: BuildHandler + + private var key: CacheKey? + private var cachedHandle: HandlerType! + private let lock = NSLock() + + fileprivate init(forBuilding buildHandler: @escaping BuildHandler) { + self.buildHandler = buildHandler + } + + func getHandler( + with arguments: Arguments, + cacheData: HandlerData + ) -> HandlerType { + lock.lock(); defer { + lock.unlock() + } + + if key == nil || key! != cacheData { + cachedHandle = buildHandler(arguments) + key = .init(data: cacheData) + } + return cachedHandle + } +} + +let cacheHandlerFactory = HandlerFactory { ( + tilesConfig: TilesConfig, + config: ConfigHandle, + historyRecorder: HistoryRecorderHandle? +) in + CacheFactory.build( + for: tilesConfig, + config: config, + historyRecorder: historyRecorder, + frameworkTypeForSKU: .CF + ) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Movement/NavigationMovementMonitor.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Movement/NavigationMovementMonitor.swift new file mode 100644 index 000000000..ecef4aed7 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Movement/NavigationMovementMonitor.swift @@ -0,0 +1,84 @@ +import _MapboxNavigationHelpers +import Foundation +import MapboxCommon_Private +import MapboxDirections + +final class NavigationMovementMonitor: MovementMonitorInterface { + private var observers: [any MovementModeObserver] { + _observers.read() + } + + var currentProfile: ProfileIdentifier? { + get { + _currentProfile.read() + } + set { + _currentProfile.update(newValue) + notify(with: movementInfo) + } + } + + private let _observers: UnfairLocked<[any MovementModeObserver]> = .init([]) + private let _currentProfile: UnfairLocked = .init(nil) + private let _customMovementInfo: UnfairLocked = .init(nil) + + func getMovementInfo(forCallback callback: @escaping MovementInfoCallback) { + callback(.init(value: movementInfo)) + } + + func setMovementInfoForMode(_ movementInfo: MovementInfo) { + _customMovementInfo.update(movementInfo) + notify(with: movementInfo) + } + + func registerObserver(for observer: any MovementModeObserver) { + _observers.mutate { + $0.append(observer) + } + } + + func unregisterObserver(for observer: any MovementModeObserver) { + _observers.mutate { + $0.removeAll(where: { $0 === observer }) + } + } + + private func notify(with movementInfo: MovementInfo) { + let currentObservers = observers + currentObservers.forEach { + $0.onMovementModeChanged(for: movementInfo) + } + } + + private var movementInfo: MovementInfo { + if let customMovementInfo = _customMovementInfo.read() { + return customMovementInfo + } + let profile = currentProfile + let movementModes: [NSNumber: NSNumber] = if let movementMode = profile?.movementMode { + [100: movementMode.rawValue as NSNumber] + } else if profile != nil { + [50: MovementMode.inVehicle.rawValue as NSNumber] + } else { + [50: MovementMode.unknown.rawValue as NSNumber] + } + return MovementInfo(movementMode: movementModes, movementProvider: .SDK) + } +} + +extension MovementInfo: @unchecked Sendable {} + +extension ProfileIdentifier { + var movementMode: MovementMode? { + switch self { + case .automobile, .automobileAvoidingTraffic: + .inVehicle + case .cycling: + .cycling + case .walking: + .onFoot + default: + nil + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/NativeHandlersFactory.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/NativeHandlersFactory.swift new file mode 100644 index 000000000..4e56c3312 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/NativeHandlersFactory.swift @@ -0,0 +1,289 @@ +import _MapboxNavigationHelpers +import Foundation +import MapboxCommon +import MapboxCommon_Private +import MapboxDirections +import MapboxNavigationNative +import MapboxNavigationNative_Private + +public let customConfigKey = "com.mapbox.navigation.custom-config" +public let customConfigFeaturesKey = "features" + +/// Internal class, designed for handling initialisation of various NavigationNative entities. +/// +/// Such entities might be used not only as a part of Navigator init sequece, so it is meant not to rely on it's +/// settings. +final class NativeHandlersFactory: @unchecked Sendable { + // MARK: - Settings + + let tileStorePath: String + let apiConfiguration: ApiConfiguration + let tilesVersion: String + let targetVersion: String? + let configFactoryType: ConfigFactory.Type + let datasetProfileIdentifier: ProfileIdentifier + let routingProviderSource: MapboxNavigationNative.RouterType? + + let liveIncidentsOptions: IncidentsConfig? + let navigatorPredictionInterval: TimeInterval? + let statusUpdatingSettings: StatusUpdatingSettings? + let utilizeSensorData: Bool + let historyDirectoryURL: URL? + let initialManeuverAvoidanceRadius: TimeInterval + var locale: Locale { + didSet { + _navigator?.locale = locale + } + } + + init( + tileStorePath: String, + apiConfiguration: ApiConfiguration, + tilesVersion: String, + targetVersion: String? = nil, + configFactoryType: ConfigFactory.Type = ConfigFactory.self, + datasetProfileIdentifier: ProfileIdentifier, + routingProviderSource: MapboxNavigationNative.RouterType? = nil, + liveIncidentsOptions: IncidentsConfig?, + navigatorPredictionInterval: TimeInterval?, + statusUpdatingSettings: StatusUpdatingSettings? = nil, + utilizeSensorData: Bool, + historyDirectoryURL: URL?, + initialManeuverAvoidanceRadius: TimeInterval, + locale: Locale + ) { + self.tileStorePath = tileStorePath + self.apiConfiguration = apiConfiguration + self.tilesVersion = tilesVersion + self.targetVersion = targetVersion + self.configFactoryType = configFactoryType + self.datasetProfileIdentifier = datasetProfileIdentifier + self.routingProviderSource = routingProviderSource + + self.liveIncidentsOptions = liveIncidentsOptions + self.navigatorPredictionInterval = navigatorPredictionInterval + self.statusUpdatingSettings = statusUpdatingSettings + self.utilizeSensorData = utilizeSensorData + self.historyDirectoryURL = historyDirectoryURL + self.initialManeuverAvoidanceRadius = initialManeuverAvoidanceRadius + self.locale = locale + } + + func targeting(version: String?) -> NativeHandlersFactory { + return .init( + tileStorePath: tileStorePath, + apiConfiguration: apiConfiguration, + tilesVersion: tilesVersion, + targetVersion: version, + configFactoryType: configFactoryType, + datasetProfileIdentifier: datasetProfileIdentifier, + routingProviderSource: routingProviderSource, + liveIncidentsOptions: liveIncidentsOptions, + navigatorPredictionInterval: navigatorPredictionInterval, + statusUpdatingSettings: statusUpdatingSettings, + utilizeSensorData: utilizeSensorData, + historyDirectoryURL: historyDirectoryURL, + initialManeuverAvoidanceRadius: initialManeuverAvoidanceRadius, + locale: locale + ) + } + + // MARK: - Native Handlers + + lazy var historyRecorderHandle: HistoryRecorderHandle? = onMainQueueSync { + historyDirectoryURL.flatMap { + HistoryRecorderHandle.build( + forHistoryDir: $0.path, + config: configHandle(by: configFactoryType) + ) + } + } + + private var _navigator: NavigationNativeNavigator? + var navigator: NavigationNativeNavigator { + if let _navigator { + return _navigator + } + return onMainQueueSync { + // Make sure that Navigator pick ups Main Thread RunLoop. + let historyRecorder = historyRecorderHandle + let configHandle = configHandle(by: configFactoryType) + let navigator = if let routingProviderSource { + MapboxNavigationNative.Navigator( + config: configHandle, + cache: cacheHandle, + historyRecorder: historyRecorder, + routerTypeRestriction: routingProviderSource + ) + } else { + MapboxNavigationNative.Navigator( + config: configHandle, + cache: cacheHandle, + historyRecorder: historyRecorder + ) + } + + let nativeNavigator = NavigationNativeNavigator(navigator: navigator, locale: locale) + self._navigator = nativeNavigator + return nativeNavigator + } + } + + lazy var cacheHandle: CacheHandle = cacheHandlerFactory.getHandler( + with: ( + tilesConfig: tilesConfig, + configHandle: configHandle(by: configFactoryType), + historyRecorder: historyRecorderHandle + ), + cacheData: self + ) + + lazy var roadGraph: RoadGraph = .init(MapboxNavigationNative.GraphAccessor(cache: cacheHandle)) + + lazy var tileStore: TileStore = .__create(forPath: tileStorePath) + + // MARK: - Support Objects + + static var settingsProfile: SettingsProfile { + SettingsProfile( + application: .mobile, + platform: .IOS + ) + } + + lazy var endpointConfig: TileEndpointConfiguration = .init( + apiConfiguration: apiConfiguration, + tilesVersion: tilesVersion, + minimumDaysToPersistVersion: nil, + targetVersion: targetVersion, + datasetProfileIdentifier: datasetProfileIdentifier + ) + + lazy var tilesConfig: TilesConfig = .init( + tilesPath: tileStorePath, + tileStore: tileStore, + inMemoryTileCache: nil, + onDiskTileCache: nil, + endpointConfig: endpointConfig, + hdEndpointConfig: nil + ) + + var navigatorConfig: NavigatorConfig { + var nativeIncidentsOptions: MapboxNavigationNative.IncidentsOptions? + if let incidentsOptions = liveIncidentsOptions, + !incidentsOptions.graph.isEmpty + { + nativeIncidentsOptions = .init( + graph: incidentsOptions.graph, + apiUrl: incidentsOptions.apiURL?.absoluteString ?? "" + ) + } + + var pollingConfig: PollingConfig? = nil + + if let predictionInterval = navigatorPredictionInterval { + pollingConfig = PollingConfig( + lookAhead: NSNumber(value: predictionInterval), + unconditionalPatience: nil, + unconditionalInterval: nil + ) + } + if let config = statusUpdatingSettings { + if pollingConfig != nil { + pollingConfig?.unconditionalInterval = config.updatingInterval.map { NSNumber(value: $0) } + pollingConfig?.unconditionalPatience = config.updatingPatience.map { NSNumber(value: $0) } + } else if config.updatingPatience != nil || config.updatingInterval != nil { + pollingConfig = PollingConfig( + lookAhead: nil, + unconditionalPatience: config.updatingPatience + .map { NSNumber(value: $0) }, + unconditionalInterval: config.updatingInterval + .map { NSNumber(value: $0) } + ) + } + } + + return NavigatorConfig( + voiceInstructionThreshold: nil, + electronicHorizonOptions: nil, + polling: pollingConfig, + incidentsOptions: nativeIncidentsOptions, + noSignalSimulationEnabled: nil, + useSensors: NSNumber(booleanLiteral: utilizeSensorData) + ) + } + + func configHandle(by configFactoryType: ConfigFactory.Type = ConfigFactory.self) -> ConfigHandle { + let defaultConfig = [ + customConfigFeaturesKey: [ + "useInternalReroute": true, + "useTelemetryNavigationEvents": true, + ], + "navigation": [ + "alternativeRoutes": [ + "dropDistance": [ + "maxSlightFork": 50.0, + ], + ], + ], + ] + + var customConfig = UserDefaults.standard.dictionary(forKey: customConfigKey) ?? [:] + customConfig.deepMerge(with: defaultConfig, uniquingKeysWith: { first, _ in first }) + + let customConfigJSON: String + if let jsonDataConfig = try? JSONSerialization.data(withJSONObject: customConfig, options: []), + let encodedConfig = String(data: jsonDataConfig, encoding: .utf8) + { + customConfigJSON = encodedConfig + } else { + assertionFailure("Custom config can not be serialized") + customConfigJSON = "" + } + + let configHandle = configFactoryType.build( + for: Self.settingsProfile, + config: navigatorConfig, + customConfig: customConfigJSON + ) + let avoidManeuverSeconds = NSNumber(value: initialManeuverAvoidanceRadius) + configHandle.mutableSettings().setAvoidManeuverSecondsForSeconds(avoidManeuverSeconds) + + configHandle.mutableSettings().setUserLanguagesForLanguages(locale.preferredBCP47Codes) + return configHandle + } + + @MainActor + func telemetry(eventsMetadataProvider: EventsMetadataInterface) -> Telemetry { + navigator.native.getTelemetryForEventsMetadataProvider(eventsMetadataProvider) + } +} + +extension TileEndpointConfiguration { + /// Initializes an object that configures a navigator to obtain routing tiles of the given version from an endpoint, + /// using the given credentials. + /// - Parameters: + /// - apiConfiguration: ApiConfiguration for accessing road network data. + /// - tilesVersion: Routing tile version. + /// - minimumDaysToPersistVersion: The minimum age in days that a tile version much reach before a new version can + /// be requested from the tile endpoint. + /// - targetVersion: Routing tile version, which navigator would like to eventually switch to if it becomes + /// available + /// - datasetProfileIdentifier profile setting, used for selecting tiles type for navigation. + convenience init( + apiConfiguration: ApiConfiguration, + tilesVersion: String, + minimumDaysToPersistVersion: Int?, + targetVersion: String?, + datasetProfileIdentifier: ProfileIdentifier + ) { + self.init( + host: apiConfiguration.endPoint.absoluteString, + dataset: datasetProfileIdentifier.rawValue, + version: tilesVersion, + isFallback: targetVersion != nil, + versionBeforeFallback: targetVersion ?? tilesVersion, + minDiffInDaysToConsiderServerVersion: minimumDaysToPersistVersion as NSNumber? + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/RoutesCoordinator.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/RoutesCoordinator.swift new file mode 100644 index 000000000..f531cc367 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/RoutesCoordinator.swift @@ -0,0 +1,103 @@ +import Foundation +import MapboxNavigationNative + +/// Coordinating routes update to NavNative Navigator to rule out some edge scenarios. +final class RoutesCoordinator { + private enum State { + case passiveNavigation + case activeNavigation(UUID) + } + + typealias RoutesResult = (mainRouteInfo: RouteInfo?, alternativeRoutes: [RouteAlternative]) + typealias RoutesSetupHandler = @MainActor ( + _ routesData: RoutesData?, + _ legIndex: UInt32, + _ reason: SetRoutesReason, + _ completion: @escaping (Result) -> Void + ) -> Void + typealias AlternativeRoutesSetupHandler = @MainActor ( + _ routes: [RouteInterface], + _ completion: @escaping (Result<[RouteAlternative], Error>) -> Void + ) -> Void + + private struct ActiveNavigationSession { + let uuid: UUID + } + + private let routesSetupHandler: RoutesSetupHandler + private let alternativeRoutesSetupHandler: AlternativeRoutesSetupHandler + /// The lock that protects mutable state in `RoutesCoordinator`. + private let lock: NSLock + private var state: State + + /// Create a new coordinator that will coordinate requests to set main and alternative routes. + /// - Parameter routesSetupHandler: The handler that passes main and alternative route's`RouteInterface` objects to + /// underlying Navigator. + /// - Parameter alternativeRoutesSetupHandler: The handler that passes only alternative route's`RouteInterface` + /// objects to underlying Navigator. Main route must be set before and it will remain unchanged. + init( + routesSetupHandler: @escaping RoutesSetupHandler, + alternativeRoutesSetupHandler: @escaping AlternativeRoutesSetupHandler + ) { + self.routesSetupHandler = routesSetupHandler + self.alternativeRoutesSetupHandler = alternativeRoutesSetupHandler + self.lock = .init() + self.state = .passiveNavigation + } + + /// - Parameters: + /// - uuid: The UUID of the current active guidances session. All reroutes should have the same uuid. + /// - legIndex: The index of the leg along which to begin navigating. + @MainActor + func beginActiveNavigation( + with routesData: RoutesData, + uuid: UUID, + legIndex: UInt32, + reason: SetRoutesReason, + completion: @escaping (Result) -> Void + ) { + lock.lock() + if case .activeNavigation(let currentUUID) = state, currentUUID != uuid { + Log.fault( + "[BUG] Two simultaneous active navigation sessions. This might happen if there are two NavigationViewController or RouteController instances exists at the same time. Profile the app and make sure that NavigationViewController is deallocated once not in use.", + category: .navigation + ) + } + + state = .activeNavigation(uuid) + lock.unlock() + + routesSetupHandler(routesData, legIndex, reason, completion) + } + + /// - Parameters: + /// - uuid: The UUID that was passed to `RoutesCoordinator.beginActiveNavigation(with:uuid:completion:)` method. + @MainActor + func endActiveNavigation(with uuid: UUID, completion: @escaping (Result) -> Void) { + lock.lock() + guard case .activeNavigation(let currentUUID) = state, currentUUID == uuid else { + lock.unlock() + completion(.failure(RoutesCoordinatorError.endingInvalidActiveNavigation)) + return + } + state = .passiveNavigation + lock.unlock() + routesSetupHandler(nil, 0, .cleanUp, completion) + } + + @MainActor + func updateAlternativeRoutes( + with routes: [RouteInterface], + completion: @escaping (Result<[RouteAlternative], Error>) -> Void + ) { + alternativeRoutesSetupHandler(routes, completion) + } +} + +enum RoutesCoordinatorError: Swift.Error { + /// `RoutesCoordinator.beginActiveNavigation(with:uuid:completion:)` called while the previous navigation wasn't + /// ended with `RoutesCoordinator.endActiveNavigation(with:completion:)` method. + /// + /// It is most likely a sign of a programmer error in the app code. + case endingInvalidActiveNavigation +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/DispatchTimer.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/DispatchTimer.swift new file mode 100644 index 000000000..2663029fd --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/DispatchTimer.swift @@ -0,0 +1,101 @@ +import Dispatch +import Foundation + +/// `DispatchTimer` is a general-purpose wrapper over the `DispatchSourceTimer` mechanism in GCD. +class DispatchTimer { + /// The state of a `DispatchTimer`. + enum State { + /// Timer is active and has an event scheduled. + case armed + /// Timer is idle. + case disarmed + } + + typealias Payload = @Sendable () -> Void + static let defaultAccuracy: DispatchTimeInterval = .milliseconds(500) + + /// Timer current state. + private(set) var state: State = .disarmed + + var countdownInterval: DispatchTimeInterval { + didSet { + reset() + } + } + + private var deadline: DispatchTime { return .now() + countdownInterval } + let repetitionInterval: DispatchTimeInterval + let accuracy: DispatchTimeInterval + let payload: Payload + let timerQueue = DispatchQueue(label: "com.mapbox.SimulatedLocationManager.Timer") + let executionQueue: DispatchQueue + let timer: DispatchSourceTimer + + /// Initializes a new timer. + /// - Parameters: + /// - countdown: The initial time interval for the timer to wait before firing off the payload for the first time. + /// - repetition: The subsequent time interval for the timer to wait before firing off the payload an additional + /// time. Repeats until manually stopped. + /// - accuracy: The amount of leeway, expressed as a time interval, that the timer has in it's timing of the + /// payload execution. Default is 500 milliseconds. + /// - executionQueue: The queue on which the timer executes. Default is main queue. + /// - payload: The payload that executes when the timer expires. + init( + countdown: DispatchTimeInterval, + repeating repetition: DispatchTimeInterval = .never, + accuracy: DispatchTimeInterval = defaultAccuracy, + executingOn executionQueue: DispatchQueue = .main, + payload: @escaping Payload + ) { + self.countdownInterval = countdown + self.repetitionInterval = repetition + self.executionQueue = executionQueue + self.payload = payload + self.accuracy = accuracy + self.timer = DispatchSource.makeTimerSource(flags: [], queue: timerQueue) + } + + deinit { + timer.setEventHandler {} + timer.cancel() + // If the timer is suspended, calling cancel without resuming triggers a crash. This is documented here + // https://forums.developer.apple.com/thread/15902 + if state == .disarmed { + timer.resume() + } + } + + /// Arm the timer. Countdown will begin after this function returns. + func arm() { + guard state == .disarmed, !timer.isCancelled else { return } + state = .armed + scheduleTimer() + timer.setEventHandler { [weak self] in + if let unwrappedSelf = self { + let payload = unwrappedSelf.payload + unwrappedSelf.executionQueue.async(execute: payload) + } + } + timer.resume() + } + + /// Re-arm the timer. Countdown will restart after this function returns. + func reset() { + guard state == .armed, !timer.isCancelled else { return } + timer.suspend() + scheduleTimer() + timer.resume() + } + + /// Disarm the timer. Countdown will stop after this function returns. + func disarm() { + guard state == .armed, !timer.isCancelled else { return } + state = .disarmed + timer.suspend() + timer.setEventHandler {} + } + + private func scheduleTimer() { + timer.schedule(deadline: deadline, repeating: repetitionInterval, leeway: accuracy) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/SimulatedLocationManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/SimulatedLocationManager.swift new file mode 100644 index 000000000..4607882be --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/SimulatedLocationManager.swift @@ -0,0 +1,493 @@ +import _MapboxNavigationHelpers +import Combine +import CoreLocation +import Foundation +import MapboxDirections +import Turf + +private let maximumSpeed: CLLocationSpeed = 30 // ~108 kmh +private let minimumSpeed: CLLocationSpeed = 6 // ~21 kmh +private let verticalAccuracy: CLLocationAccuracy = 10 +private let horizontalAccuracy: CLLocationAccuracy = 40 +// minimumSpeed will be used when a location have maximumTurnPenalty +private let maximumTurnPenalty: CLLocationDirection = 90 +// maximumSpeed will be used when a location have minimumTurnPenalty +private let minimumTurnPenalty: CLLocationDirection = 0 +// Go maximum speed if distance to nearest coordinate is >= `safeDistance` +private let safeDistance: CLLocationDistance = 50 + +private class SimulatedLocation: CLLocation, @unchecked Sendable { + var turnPenalty: Double = 0 + + override var description: String { + return "\(super.description) \(turnPenalty)" + } +} + +final class SimulatedLocationManager: NavigationLocationManager, @unchecked Sendable { + @MainActor + init(initialLocation: CLLocation?) { + self.simulatedLocation = initialLocation + + super.init() + + restartTimer() + } + + // MARK: Overrides + + override func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + realLocation = locations.last + } + + // MARK: Specifying Simulation + + private func restartTimer() { + let isArmed = timer?.state == .armed + timer = DispatchTimer( + countdown: .milliseconds(0), + repeating: .milliseconds(updateIntervalMilliseconds / Int(speedMultiplier)), + accuracy: accuracy, + executingOn: queue + ) { [weak self] in + self?.tick() + } + if isArmed { + timer.arm() + } + } + + var speedMultiplier: Double = 1 { + didSet { + restartTimer() + } + } + + override var location: CLLocation? { + get { + simulatedLocation ?? realLocation + } + set { + simulatedLocation = newValue + } + } + + fileprivate var realLocation: CLLocation? + fileprivate var simulatedLocation: CLLocation? + + override var simulatesLocation: Bool { + get { return true } + set { super.simulatesLocation = newValue } + } + + override func startUpdatingLocation() { + timer.arm() + super.startUpdatingLocation() + } + + override func stopUpdatingLocation() { + timer.disarm() + super.stopUpdatingLocation() + } + + // MARK: Simulation Logic + + private var currentDistance: CLLocationDistance = 0 + private var currentSpeed: CLLocationSpeed = 0 + private let accuracy: DispatchTimeInterval = .milliseconds(50) + private let updateIntervalMilliseconds: Int = 1000 + private let defaultTickInterval: TimeInterval = 1 + private var timer: DispatchTimer! + private var locations: [SimulatedLocation]! + private var remainingRouteShape: LineString! + + private let queue = DispatchQueue(label: "com.mapbox.SimulatedLocationManager") + + private(set) var route: Route? + private var routeProgress: RouteProgress? + + private var _nextDate: Date? + private func getNextDate() -> Date { + if _nextDate == nil || _nextDate! < Date() { + _nextDate = Date() + } else { + _nextDate?.addTimeInterval(defaultTickInterval) + } + return _nextDate! + } + + private var slicedIndex: Int? + + private func update(route: Route?) { + // NOTE: this method is expected to be called on the main thread, onMainQueueSync is used as extra check + onMainAsync { [weak self] in + self?.route = route + if let shape = route?.shape { + self?.queue.async { [shape, weak self] in + self?.reset(with: shape) + } + } + } + } + + private func reset(with shape: LineString?) { + guard let shape else { return } + + remainingRouteShape = shape + locations = shape.coordinates.simulatedLocationsWithTurnPenalties() + } + + func tick() { + let ( + expectedSegmentTravelTimes, + originalShape + ) = onMainQueueSync { + ( + routeProgress?.currentLeg.expectedSegmentTravelTimes, + route?.shape + ) + } + + let tickDistance = currentSpeed * defaultTickInterval + guard let remainingShape = remainingRouteShape, + let originalShape, + let indexedNewCoordinate = remainingShape.indexedCoordinateFromStart(distance: tickDistance) + else { + // report last known coordinate or real one + if let simulatedLocation { + self.simulatedLocation = .init(simulatedLocation: simulatedLocation, timestamp: getNextDate()) + } else if #available(iOS 15.0, *), + let realLocation, + let sourceInformation = realLocation.sourceInformation, + sourceInformation.isSimulatedBySoftware + { + // The location is simulated, we need to update timestamp + self.realLocation = .init( + simulatedLocation: realLocation, + timestamp: getNextDate(), + sourceInformation: sourceInformation + ) + } + location.map { locationDelegate?.navigationLocationManager(self, didReceiveNewLocation: $0) } + return + } + if remainingShape.distance() == 0, + let routeDistance = originalShape.distance(), + let lastCoordinate = originalShape.coordinates.last + { + currentDistance = routeDistance + currentSpeed = 0 + + let location = CLLocation( + coordinate: lastCoordinate, + altitude: 0, + horizontalAccuracy: horizontalAccuracy, + verticalAccuracy: verticalAccuracy, + course: 0, + speed: currentSpeed, + timestamp: getNextDate() + ) + onMainQueueSync { [weak self] in + guard let self else { return } + locationDelegate?.navigationLocationManager(self, didReceiveNewLocation: location) + } + + return + } + + let newCoordinate = indexedNewCoordinate.coordinate + // Closest coordinate ahead + guard let lookAheadCoordinate = remainingShape.coordinateFromStart(distance: tickDistance + 10) else { return } + guard let closestCoordinateOnRouteIndex = slicedIndex.map({ idx -> Int? in + originalShape.closestCoordinate( + to: newCoordinate, + startingIndex: idx + )?.index + }) ?? originalShape.closestCoordinate(to: newCoordinate)?.index else { return } + + // Simulate speed based on expected segment travel time + if let expectedSegmentTravelTimes, + let nextCoordinateOnRoute = originalShape.coordinates.after(index: closestCoordinateOnRouteIndex), + let time = expectedSegmentTravelTimes.optional[closestCoordinateOnRouteIndex] + { + let distance = originalShape.coordinates[closestCoordinateOnRouteIndex].distance(to: nextCoordinateOnRoute) + currentSpeed = min(max(distance / time, minimumSpeed), maximumSpeed) + slicedIndex = max(closestCoordinateOnRouteIndex - 1, 0) + } else { + let closestLocation = locations[closestCoordinateOnRouteIndex] + let distanceToClosest = closestLocation.distance(from: CLLocation(newCoordinate)) + let distance = min(max(distanceToClosest, 10), safeDistance) + let coordinatesNearby = remainingShape.trimmed(from: newCoordinate, distance: 100)!.coordinates + currentSpeed = calculateCurrentSpeed( + distance: distance, + coordinatesNearby: coordinatesNearby, + closestLocation: closestLocation + ) + } + + let location = CLLocation( + coordinate: newCoordinate, + altitude: 0, + horizontalAccuracy: horizontalAccuracy, + verticalAccuracy: verticalAccuracy, + course: newCoordinate.direction(to: lookAheadCoordinate).wrap(min: 0, max: 360), + speed: currentSpeed, + timestamp: getNextDate() + ) + + simulatedLocation = location + + onMainQueueSync { + locationDelegate?.navigationLocationManager( + self, + didReceiveNewLocation: location + ) + } + currentDistance += remainingShape.distance(to: newCoordinate) ?? 0 + remainingRouteShape = remainingShape.sliced(from: newCoordinate) + } + + func progressDidChange(_ progress: RouteProgress?) { + guard let progress else { + cleanUp() + return + } + onMainQueueSync { + self.routeProgress = progress + if progress.route.distance != self.route?.distance { + update(route: progress.route) + } + } + } + + func cleanUp() { + route = nil + routeProgress = nil + remainingRouteShape = nil + locations = [] + } + + func didReroute(progress: RouteProgress?) { + guard let progress else { return } + + update(route: progress.route) + + let shape = progress.route.shape + let currentSpeed = currentSpeed + + queue.async { [weak self] in + guard let self, + let routeProgress else { return } + + var newClosestCoordinate: LocationCoordinate2D! + if let location, + let shape, + let closestCoordinate = shape.closestCoordinate(to: location.coordinate) + { + simulatedLocation = location + currentDistance = closestCoordinate.distance + newClosestCoordinate = closestCoordinate.coordinate + } else { + currentDistance = calculateCurrentDistance(routeProgress.distanceTraveled, speed: currentSpeed) + newClosestCoordinate = shape?.coordinateFromStart(distance: currentDistance) + } + + onMainQueueSync { + self.routeProgress = progress + self.route = progress.route + } + reset(with: shape) + remainingRouteShape = remainingRouteShape.sliced(from: newClosestCoordinate) + slicedIndex = nil + } + } +} + +// MARK: - Helpers + +extension Double { + fileprivate func scale(minimumIn: Double, maximumIn: Double, minimumOut: Double, maximumOut: Double) -> Double { + return ((maximumOut - minimumOut) * (self - minimumIn) / (maximumIn - minimumIn)) + minimumOut + } +} + +extension CLLocation { + fileprivate convenience init(_ coordinate: CLLocationCoordinate2D) { + self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) + } + + fileprivate convenience init( + simulatedLocation: CLLocation, + timestamp: Date + ) { + self.init( + coordinate: simulatedLocation.coordinate, + altitude: simulatedLocation.altitude, + horizontalAccuracy: simulatedLocation.horizontalAccuracy, + verticalAccuracy: simulatedLocation.verticalAccuracy, + course: simulatedLocation.course, + speed: simulatedLocation.speed, + timestamp: timestamp + ) + } + + @available(iOS 15.0, *) + fileprivate convenience init( + simulatedLocation: CLLocation, + timestamp: Date, + sourceInformation: CLLocationSourceInformation + ) { + self.init( + coordinate: simulatedLocation.coordinate, + altitude: simulatedLocation.altitude, + horizontalAccuracy: simulatedLocation.horizontalAccuracy, + verticalAccuracy: simulatedLocation.verticalAccuracy, + course: simulatedLocation.course, + courseAccuracy: simulatedLocation.courseAccuracy, + speed: simulatedLocation.speed, + speedAccuracy: simulatedLocation.speedAccuracy, + timestamp: timestamp, + sourceInfo: sourceInformation + ) + } +} + +extension Array where Element: Hashable { + fileprivate struct OptionalSubscript { + var elements: [Element] + subscript(index: Int) -> Element? { + return index < elements.count ? elements[index] : nil + } + } + + fileprivate var optional: OptionalSubscript { return OptionalSubscript(elements: self) } +} + +extension Array where Element: Equatable { + fileprivate func after(index: Index) -> Element? { + if index + 1 < count { + return self[index + 1] + } + return nil + } +} + +extension [CLLocationCoordinate2D] { + // Calculate turn penalty for each coordinate. + fileprivate func simulatedLocationsWithTurnPenalties() -> [SimulatedLocation] { + var locations = [SimulatedLocation]() + + for (coordinate, nextCoordinate) in zip(prefix(upTo: endIndex - 1), suffix(from: 1)) { + let currentCoordinate = locations.isEmpty ? first! : coordinate + let course = coordinate.direction(to: nextCoordinate).wrap(min: 0, max: 360) + let turnPenalty = currentCoordinate.direction(to: coordinate) + .difference(from: coordinate.direction(to: nextCoordinate)) + let location = SimulatedLocation( + coordinate: coordinate, + altitude: 0, + horizontalAccuracy: horizontalAccuracy, + verticalAccuracy: verticalAccuracy, + course: course, + speed: minimumSpeed, + timestamp: Date() + ) + location.turnPenalty = Swift.max(Swift.min(turnPenalty, maximumTurnPenalty), minimumTurnPenalty) + locations.append(location) + } + + locations.append(SimulatedLocation( + coordinate: last!, + altitude: 0, + horizontalAccuracy: horizontalAccuracy, + verticalAccuracy: verticalAccuracy, + course: locations.last!.course, + speed: minimumSpeed, + timestamp: Date() + )) + + return locations + } +} + +extension LineString { + fileprivate typealias DistanceIndex = (distance: LocationDistance, index: Int) + + fileprivate func closestCoordinate(to coordinate: LocationCoordinate2D, startingIndex: Int) -> DistanceIndex? { + // Ported from https://github.com/Turfjs/turf/blob/142e137ce0c758e2825a260ab32b24db0aa19439/packages/turf-point-on-line/index.js + guard let startCoordinate = coordinates.first, + coordinates.indices.contains(startingIndex) else { return nil } + + guard coordinates.count > 1 else { + return (coordinate.distance(to: startCoordinate), 0) + } + + var closestCoordinate: DistanceIndex? + var closestDistance: LocationDistance? + + for index in startingIndex.. CLLocationDistance { + return distance + speed +} + +private func calculateCurrentSpeed( + distance: CLLocationDistance, + coordinatesNearby: [CLLocationCoordinate2D]? = nil, + closestLocation: SimulatedLocation +) -> CLLocationSpeed { + // More than 10 nearby coordinates indicates that we are in a roundabout or similar complex shape. + if let coordinatesNearby, coordinatesNearby.count >= 10 { + return minimumSpeed + } + // Maximum speed if we are a safe distance from the closest coordinate + else if distance >= safeDistance { + return maximumSpeed + } + // Base speed on previous or upcoming turn penalty + else { + let reversedTurnPenalty = maximumTurnPenalty - closestLocation.turnPenalty + return reversedTurnPenalty.scale( + minimumIn: minimumTurnPenalty, + maximumIn: maximumTurnPenalty, + minimumOut: minimumSpeed, + maximumOut: maximumSpeed + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationClient.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationClient.swift new file mode 100644 index 000000000..8e104d3c2 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationClient.swift @@ -0,0 +1,96 @@ +import Combine +import CoreLocation + +public struct LocationClient: @unchecked Sendable, Equatable { + var locations: AnyPublisher + var headings: AnyPublisher + var startUpdatingLocation: @MainActor () -> Void + var stopUpdatingLocation: @MainActor () -> Void + var startUpdatingHeading: @MainActor () -> Void + var stopUpdatingHeading: @MainActor () -> Void + + public init( + locations: AnyPublisher, + headings: AnyPublisher, + startUpdatingLocation: @escaping () -> Void, + stopUpdatingLocation: @escaping () -> Void, + startUpdatingHeading: @escaping () -> Void, + stopUpdatingHeading: @escaping () -> Void + ) { + self.locations = locations + self.headings = headings + self.startUpdatingLocation = startUpdatingLocation + self.stopUpdatingLocation = stopUpdatingLocation + self.startUpdatingHeading = startUpdatingHeading + self.stopUpdatingHeading = stopUpdatingHeading + } + + private let id = UUID().uuidString + public static func == (lhs: LocationClient, rhs: LocationClient) -> Bool { lhs.id == rhs.id } +} + +extension LocationClient { + static var liveValue: Self { + class Delegate: NSObject, CLLocationManagerDelegate { + var locations: AnyPublisher { + locationsSubject.eraseToAnyPublisher() + } + + var headings: AnyPublisher { + headingSubject.eraseToAnyPublisher() + } + + private let manager = CLLocationManager() + private let locationsSubject = PassthroughSubject() + private let headingSubject = PassthroughSubject() + + override init() { + super.init() + assert(Thread.isMainThread) // CLLocationManager has to be created on the main thread + manager.requestWhenInUseAuthorization() + + if Bundle.main.backgroundModes.contains("location") { + manager.allowsBackgroundLocationUpdates = true + } + manager.delegate = self + } + + func startUpdatingLocation() { + manager.startUpdatingLocation() + } + + func stopUpdatingLocation() { + manager.stopUpdatingLocation() + } + + func startUpdatingHeading() { + manager.startUpdatingHeading() + } + + func stopUpdatingHeading() { + manager.stopUpdatingHeading() + } + + nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + if let location = locations.last { + locationsSubject.send(location) + } + } + + nonisolated func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { + headingSubject.send(newHeading) + } + } + + let delegate = Delegate() + + return Self( + locations: delegate.locations, + headings: delegate.headings, + startUpdatingLocation: { delegate.startUpdatingLocation() }, + stopUpdatingLocation: { delegate.stopUpdatingLocation() }, + startUpdatingHeading: { delegate.startUpdatingHeading() }, + stopUpdatingHeading: { delegate.stopUpdatingHeading() } + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationSource.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationSource.swift new file mode 100644 index 000000000..038536262 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationSource.swift @@ -0,0 +1,8 @@ +import CoreLocation +import Foundation + +public enum LocationSource: Equatable, @unchecked Sendable { + case simulation(initialLocation: CLLocation? = nil) + case live + case custom(LocationClient) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/MultiplexLocationClient.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/MultiplexLocationClient.swift new file mode 100644 index 000000000..5747b63bf --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/MultiplexLocationClient.swift @@ -0,0 +1,139 @@ +import Combine +import CoreLocation +import Foundation + +/// Allows switching between sources of location data: +/// ``LocationSource/live`` which sends real GPS locations; +/// ``LocationSource/simulation(initialLocation:)`` that simulates the route traversal; +/// ``LocationSource/custom(_:)`` that allows to provide a custom location data source; +@MainActor +public final class MultiplexLocationClient: @unchecked Sendable { + private let locations = PassthroughSubject() + private let headings = PassthroughSubject() + private var currentLocationClient: LocationClient = .empty + private var isUpdating = false + private var isUpdatingHeading = false + private var routeProgress: AnyPublisher = Just(nil).eraseToAnyPublisher() + private var rerouteEvents: AnyPublisher = Just(nil).eraseToAnyPublisher() + private var currentLocationClientSubscriptions: Set = [] + + var locationClient: LocationClient { + .init( + locations: locations.eraseToAnyPublisher(), + headings: headings.eraseToAnyPublisher(), + startUpdatingLocation: { [weak self] in self?.startUpdatingLocation() }, + stopUpdatingLocation: { [weak self] in self?.stopUpdatingLocation() }, + startUpdatingHeading: { [weak self] in self?.startUpdatingHeading() }, + stopUpdatingHeading: { [weak self] in self?.stopUpdatingHeading() } + ) + } + + var isInitialized: Bool = false + + nonisolated init(source: LocationSource) { + setLocationSource(source) + } + + nonisolated func subscribeToNavigatorUpdates( + _ navigator: MapboxNavigator, + source: LocationSource + ) { + Task { @MainActor in + self.isInitialized = true + self.routeProgress = navigator.routeProgress + self.rerouteEvents = navigator.navigationRoutes + .map { _ in navigator.currentRouteProgress?.routeProgress } + .eraseToAnyPublisher() + setLocationSource(source) + } + } + + func startUpdatingLocation() { + isUpdating = true + Task { @MainActor in + currentLocationClient.startUpdatingLocation() + } + } + + func stopUpdatingLocation() { + isUpdating = false + Task { @MainActor in + currentLocationClient.stopUpdatingLocation() + } + } + + func startUpdatingHeading() { + isUpdatingHeading = true + Task { @MainActor in + currentLocationClient.startUpdatingHeading() + } + } + + func stopUpdatingHeading() { + isUpdatingHeading = false + Task { @MainActor in + currentLocationClient.stopUpdatingHeading() + } + } + + nonisolated func setLocationSource(_ source: LocationSource) { + Task { @MainActor in + let newLocationClient: LocationClient + switch source { + case .simulation(let location): + newLocationClient = .simulatedLocationManager( + routeProgress: routeProgress, + rerouteEvents: rerouteEvents, + initialLocation: location + ) + if let location { + locations.send(location) + } + case .live: + newLocationClient = .liveValue + case .custom(let customClient): + newLocationClient = customClient + } + + currentLocationClient.stopUpdatingHeading() + currentLocationClient.stopUpdatingLocation() + + if isUpdating { + newLocationClient.startUpdatingLocation() + } else { + newLocationClient.stopUpdatingLocation() + } + + if isUpdatingHeading { + newLocationClient.startUpdatingHeading() + } else { + newLocationClient.stopUpdatingHeading() + } + + currentLocationClient = newLocationClient + currentLocationClientSubscriptions.removeAll() + + newLocationClient.locations + .subscribe(on: DispatchQueue.main) + .sink { [weak self] in + self?.locations.send($0) + } + .store(in: ¤tLocationClientSubscriptions) + newLocationClient.headings + .subscribe(on: DispatchQueue.main) + .sink { [weak self] in self?.headings.send($0) } + .store(in: ¤tLocationClientSubscriptions) + } + } +} + +extension LocationClient { + fileprivate static let empty = LocationClient( + locations: Empty().eraseToAnyPublisher(), + headings: Empty().eraseToAnyPublisher(), + startUpdatingLocation: {}, + stopUpdatingLocation: {}, + startUpdatingHeading: {}, + stopUpdatingHeading: {} + ) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/SimulatedLocationManagerWrapper.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/SimulatedLocationManagerWrapper.swift new file mode 100644 index 000000000..76c3b9924 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/SimulatedLocationManagerWrapper.swift @@ -0,0 +1,73 @@ +import Combine +import CoreLocation + +extension LocationClient { + @MainActor + static func simulatedLocationManager( + routeProgress: AnyPublisher, + rerouteEvents: AnyPublisher, + initialLocation: CLLocation? + ) -> Self { + let wrapper = SimulatedLocationManagerWrapper( + routeProgress: routeProgress, + rerouteEvents: rerouteEvents, + initialLocation: initialLocation + ) + return Self( + locations: wrapper.locations, + headings: Empty().eraseToAnyPublisher(), + startUpdatingLocation: { + wrapper.startUpdatingLocation() + }, + stopUpdatingLocation: { + wrapper.stopUpdatingLocation() + }, + startUpdatingHeading: {}, + stopUpdatingHeading: {} + ) + } +} + +@MainActor +private class SimulatedLocationManagerWrapper: NavigationLocationManagerDelegate { + private let manager: SimulatedLocationManager + private let _locations = PassthroughSubject() + private var lifetimeSubscriptions: Set = [] + + var locations: AnyPublisher { _locations.eraseToAnyPublisher() } + + @MainActor + init( + routeProgress: AnyPublisher, + rerouteEvents: AnyPublisher, + initialLocation: CLLocation? + ) { + self.manager = SimulatedLocationManager(initialLocation: initialLocation) + manager.locationDelegate = self + + routeProgress.sink { [weak self] in + self?.manager.progressDidChange($0?.routeProgress) + }.store(in: &lifetimeSubscriptions) + + rerouteEvents.sink { [weak self] in + self?.manager.didReroute(progress: $0) + }.store(in: &lifetimeSubscriptions) + } + + func startUpdatingLocation() { + manager.startUpdatingLocation() + } + + func stopUpdatingLocation() { + manager.stopUpdatingLocation() + } + + nonisolated func navigationLocationManager( + _ locationManager: NavigationLocationManager, + didReceiveNewLocation location: CLLocation + ) { + Task { @MainActor in + self._locations.send(location) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapMatchingResult.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapMatchingResult.swift new file mode 100644 index 000000000..d68df8bfa --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapMatchingResult.swift @@ -0,0 +1,79 @@ +import CoreLocation +import Foundation +import MapboxNavigationNative + +/// Provides information about the status of the enhanced location updates generated by the map matching engine of the +/// Navigation SDK. +public struct MapMatchingResult: Equatable, @unchecked Sendable { + /// The best possible location update, snapped to the route or map matched to the road if possible + public var enhancedLocation: CLLocation + + /// A list of predicted location points leading up to the target update. + /// + /// The last point on the list (if it is not empty) is always equal to `enhancedLocation`. + public var keyPoints: [CLLocation] + + /// Whether the SDK thinks that the user is off road. + /// + /// Based on the `offRoadProbability`. + public var isOffRoad: Bool + + /// Probability that the user is off road. + public var offRoadProbability: Double + + /// Returns true if map matcher changed its opinion about most probable path on last update. + /// + /// In practice it means we don't need to animate puck movement from previous to current location and just do an + /// immediate transition instead. + public var isTeleport: Bool + + /// When map matcher snaps to a road, this is the confidence in the chosen edge from all nearest edges. + public var roadEdgeMatchProbability: Double + + /// Creates a new `MapMatchingResult` with given parameters + /// + /// It is not intended for user to create his own `MapMatchingResult` except for testing purposes. + @_documentation(visibility: internal) + public init( + enhancedLocation: CLLocation, + keyPoints: [CLLocation], + isOffRoad: Bool, + offRoadProbability: Double, + isTeleport: Bool, + roadEdgeMatchProbability: Double + ) { + self.enhancedLocation = enhancedLocation + self.keyPoints = keyPoints + self.isOffRoad = isOffRoad + self.offRoadProbability = offRoadProbability + self.isTeleport = isTeleport + self.roadEdgeMatchProbability = roadEdgeMatchProbability + } + + init(status: NavigationStatus) { + self.enhancedLocation = CLLocation(status.location) + self.keyPoints = status.keyPoints.map { CLLocation($0) } + self.isOffRoad = status.offRoadProba > 0.5 + self.offRoadProbability = Double(status.offRoadProba) + self.isTeleport = status.mapMatcherOutput.isTeleport + self.roadEdgeMatchProbability = Double(status.mapMatcherOutput.matches.first?.proba ?? 0.0) + } +} + +extension CLLocation { + convenience init(_ location: FixLocation) { + let timestamp = Date(timeIntervalSince1970: TimeInterval(location.monotonicTimestampNanoseconds) / 1e9) + self.init( + coordinate: location.coordinate, + altitude: location.altitude?.doubleValue ?? 0, + horizontalAccuracy: location.accuracyHorizontal?.doubleValue ?? -1, + verticalAccuracy: location.verticalAccuracy?.doubleValue ?? -1, + course: location.bearing?.doubleValue ?? -1, + courseAccuracy: location.bearingAccuracy?.doubleValue ?? -1, + // TODO: investigate why we need 0 when in v2 we used to have -1 + speed: location.speed?.doubleValue ?? 0, + speedAccuracy: location.speedAccuracy?.doubleValue ?? -1, + timestamp: timestamp + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapboxNavigator.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapboxNavigator.swift new file mode 100644 index 000000000..f2ecce96a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapboxNavigator.swift @@ -0,0 +1,1414 @@ +import _MapboxNavigationHelpers +import Combine +import Foundation +import MapboxDirections +@preconcurrency import MapboxNavigationNative + +final class MapboxNavigator: @unchecked Sendable { + struct Configuration: @unchecked Sendable { + let navigator: CoreNavigator + let routeParserType: RouteParser.Type + let locationClient: LocationClient + let alternativesAcceptionPolicy: AlternativeRoutesDetectionConfig.AcceptionPolicy? + let billingHandler: BillingHandler + let multilegAdvancing: CoreConfig.MultilegAdvanceMode + let prefersOnlineRoute: Bool + let disableBackgroundTrackingLocation: Bool + let fasterRouteController: FasterRouteProvider? + let electronicHorizonConfig: ElectronicHorizonConfig? + let congestionConfig: CongestionRangesConfiguration + let movementMonitor: NavigationMovementMonitor + } + + // MARK: - Navigator Implementation + + @CurrentValuePublisher var session: AnyPublisher + @MainActor + var currentSession: Session { + _session.value + } + + @CurrentValuePublisher var routeProgress: AnyPublisher + var currentRouteProgress: RouteProgressState? { + _routeProgress.value + } + + @CurrentValuePublisher var mapMatching: AnyPublisher + @MainActor + var currentMapMatching: MapMatchingState? { + _mapMatching.value + } + + @EventPublisher var offlineFallbacks + + @EventPublisher var voiceInstructions + + @EventPublisher var bannerInstructions + + @EventPublisher var waypointsArrival + + @EventPublisher var rerouting + + @EventPublisher var continuousAlternatives + + @EventPublisher var fasterRoutes + + @EventPublisher var routeRefreshing + + @EventPublisher var eHorizonEvents + + @EventPublisher var errors + + var heading: AnyPublisher { + locationClient.headings + } + + @CurrentValuePublisher var navigationRoutes: AnyPublisher + var currentNavigationRoutes: NavigationRoutes? { + _navigationRoutes.value + } + + let roadMatching: RoadMatching + + @MainActor + func startActiveGuidance(with navigationRoutes: NavigationRoutes, startLegIndex: Int) { + send(navigationRoutes) + Task { + await updateRouteProgress(with: navigationRoutes) + } + taskManager.withBarrier { + setRoutes( + navigationRoutes: navigationRoutes, + startLegIndex: startLegIndex, + reason: .newRoute + ) + } + let profile = navigationRoutes.mainRoute.route.legs.first?.profileIdentifier + configuration.movementMonitor.currentProfile = profile + } + + private let statusUpdateEvents: AsyncStreamBridge + + enum SetRouteReason { + case newRoute + case reroute + case alternatives + case fasterRoute + case fallbackToOffline + case restoreToOnline + } + + private var setRoutesTask: Task? + + @MainActor + func setRoutes(navigationRoutes: NavigationRoutes, startLegIndex: Int, reason: SetRouteReason) { + verifyActiveGuidanceBillingSession(for: navigationRoutes) + + guard let sessionUUID else { + Log.error( + "Failed to set routes due to missing session ID.", + category: .billing + ) + send(NavigatorErrors.FailedToSetRoute(underlyingError: nil)) + return + } + + locationClient.startUpdatingLocation() + locationClient.startUpdatingHeading() + navigator.resume() + + navigator.setRoutes( + navigationRoutes.asRoutesData(), + uuid: sessionUUID, + legIndex: UInt32(startLegIndex), + reason: reason.navNativeValue + ) { [weak self] result in + guard let self else { return } + + setRoutesTask?.cancel() + setRoutesTask = Task.detached { + switch result { + case .success(let info): + var navigationRoutes = navigationRoutes + let alternativeRoutes = await AlternativeRoute.fromNative( + alternativeRoutes: info.alternativeRoutes, + relateveTo: navigationRoutes.mainRoute + ) + + guard !Task.isCancelled else { return } + navigationRoutes.allAlternativeRoutesWithIgnored = alternativeRoutes + await self.updateRouteProgress(with: navigationRoutes) + await self.send(navigationRoutes) + switch reason { + case .newRoute: + // Do nothing, routes updates are already sent + break + case .reroute: + await self.send( + ReroutingStatus(event: ReroutingStatus.Events.Fetched()) + ) + case .alternatives: + let event = AlternativesStatus.Events.SwitchedToAlternative(navigationRoutes: navigationRoutes) + await self.send(AlternativesStatus(event: event)) + case .fasterRoute: + await self.send(FasterRoutesStatus(event: FasterRoutesStatus.Events.Applied())) + case .fallbackToOffline: + await self.send( + FallbackToTilesState(usingLatestTiles: false) + ) + case .restoreToOnline: + await self.send(FallbackToTilesState(usingLatestTiles: true)) + } + await self.send(Session(state: .activeGuidance(.uncertain))) + case .failure(let error): + Log.error("Failed to set routes, error: \(error).", category: .navigation) + await self.send(NavigatorErrors.FailedToSetRoute(underlyingError: error)) + } + self.setRoutesTask = nil + self.rerouteController?.abortReroutePipeline = navigationRoutes.isCustomExternalRoute + } + } + } + + func selectAlternativeRoute(at index: Int) { + taskManager.cancellableTask { [self] in + guard case .activeGuidance = await currentSession.state, + let alternativeRoutes = await currentNavigationRoutes?.selectingAlternativeRoute(at: index), + !Task.isCancelled + else { + Log.warning( + "Attempt to select invalid alternative route (index '\(index)' of alternatives - '\(String(describing: currentNavigationRoutes))').", + category: .navigation + ) + await send(NavigatorErrors.FailedToSelectAlternativeRoute()) + return + } + + await setRoutes( + navigationRoutes: alternativeRoutes, + startLegIndex: 0, + reason: .alternatives + ) + } + } + + func selectAlternativeRoute(with routeId: RouteId) { + guard let index = currentNavigationRoutes?.alternativeRoutes.firstIndex(where: { $0.routeId == routeId }) else { + Log.warning( + "Attempt to select invalid alternative route with '\(routeId)' available ids - '\((currentNavigationRoutes?.alternativeRoutes ?? []).map(\.routeId))'", + category: .navigation + ); return + } + + selectAlternativeRoute(at: index) + } + + func switchLeg(newLegIndex: Int) { + taskManager.cancellableTask { @MainActor [self] in + guard case .activeGuidance = currentSession.state, + billingSessionIsActive(withType: .activeGuidance), + !Task.isCancelled + else { + Log.warning("Attempt to switch route leg while not in Active Guidance.", category: .navigation) + return + } + + navigator.updateRouteLeg(to: UInt32(newLegIndex)) { [weak self] success in + Task { [weak self] in + if success { + guard let sessionUUID = self?.sessionUUID else { + Log.error( + "Route leg switching failed due to missing session ID.", + category: .billing + ) + await self?.send(NavigatorErrors.FailedToSelectRouteLeg()) + return + } + self?.billingHandler.beginNewBillingSessionIfExists(with: sessionUUID) + let event = WaypointArrivalStatus.Events.NextLegStarted(newLegIndex: newLegIndex) + await self?.send(WaypointArrivalStatus(event: event)) + } else { + Log.warning("Route leg switching failed.", category: .navigation) + await self?.send(NavigatorErrors.FailedToSelectRouteLeg()) + } + } + } + } + } + + @MainActor + func setToIdle() { + taskManager.withBarrier { + let hadActiveGuidance = billingSessionIsActive(withType: .activeGuidance) + if let sessionUUID, + billingSessionIsActive() + { + billingHandler.pauseBillingSession(with: sessionUUID) + } + + guard currentSession.state != .idle else { + Log.warning("Duplicate setting to idle state attempted", category: .navigation) + send(NavigatorErrors.FailedToSetToIdle()) + return + } + + send(NavigationRoutes?.none) + send(RouteProgressState?.none) + locationClient.stopUpdatingLocation() + locationClient.stopUpdatingHeading() + navigator.pause() + + guard hadActiveGuidance else { + send(Session(state: .idle)) + return + } + guard let sessionUUID = self.sessionUUID else { + Log.error( + "`MapboxNavigator.setToIdle` failed to reset routes due to missing session ID.", + category: .billing + ) + send(NavigatorErrors.FailedToSetToIdle()) + return + } + + navigator.unsetRoutes(uuid: sessionUUID) { result in + Task { + if case .failure(let error) = result { + Log.warning( + "`MapboxNavigator.setToIdle` failed to reset routes with error: \(error)", + category: .navigation + ) + } + await self.send(Session(state: .idle)) + } + } + billingHandler.stopBillingSession(with: sessionUUID) + self.sessionUUID = nil + } + configuration.movementMonitor.currentProfile = nil + } + + @MainActor + func startFreeDrive() { + taskManager.withBarrier { + let activeGuidanceSession = verifyFreeDriveBillingSession() + + guard sessionUUID != nil else { + Log.error( + "`MapboxNavigator.startFreeDrive` failed to start new session due to missing session ID.", + category: .billing + ) + return + } + + send(NavigationRoutes?.none) + send(RouteProgressState?.none) + locationClient.startUpdatingLocation() + locationClient.startUpdatingHeading() + navigator.resume() + if let activeGuidanceSession { + navigator.unsetRoutes(uuid: activeGuidanceSession) { result in + Task { + if case .failure(let error) = result { + Log.warning( + "`MapboxNavigator.startFreeDrive` failed to reset routes with error: \(error)", + category: .navigation + ) + } + await self.send(Session(state: .freeDrive(.active))) + } + } + } else { + send(Session(state: .freeDrive(.active))) + } + } + } + + @MainActor + func pauseFreeDrive() { + taskManager.withBarrier { + guard case .freeDrive = currentSession.state, + let sessionUUID, + billingSessionIsActive(withType: .freeDrive) + else { + send(NavigatorErrors.FailedToPause()) + Log.warning( + "Attempt to pause navigation while not in Free Drive.", + category: .navigation + ) + return + } + locationClient.stopUpdatingLocation() + locationClient.stopUpdatingHeading() + navigator.pause() + billingHandler.pauseBillingSession(with: sessionUUID) + send(Session(state: .freeDrive(.paused))) + } + } + + func startUpdatingEHorizon() { + guard let config = configuration.electronicHorizonConfig else { + return + } + + Task { @MainActor in + navigator.startUpdatingElectronicHorizon(with: config) + } + } + + func stopUpdatingEHorizon() { + Task { @MainActor in + navigator.stopUpdatingElectronicHorizon() + } + } + + // MARK: - Billing checks + + @MainActor + private func billingSessionIsActive(withType type: BillingHandler.SessionType? = nil) -> Bool { + guard let sessionUUID, + billingHandler.sessionState(uuid: sessionUUID) == .running + else { + return false + } + + if let type, + billingHandler.sessionType(uuid: sessionUUID) != type + { + return false + } + + return true + } + + @MainActor + private func beginNewSession(of type: BillingHandler.SessionType) { + let newSession = UUID() + sessionUUID = newSession + billingHandler.beginBillingSession( + for: type, + uuid: newSession + ) + } + + @MainActor + private func verifyActiveGuidanceBillingSession(for navigationRoutes: NavigationRoutes) { + if let sessionUUID, + let sessionType = billingHandler.sessionType(uuid: sessionUUID) + { + switch sessionType { + case .freeDrive: + billingHandler.stopBillingSession(with: sessionUUID) + beginNewSession(of: .activeGuidance) + case .activeGuidance: + if billingHandler.shouldStartNewBillingSession( + for: navigationRoutes.mainRoute.route, + remainingWaypoints: currentRouteProgress?.routeProgress.remainingWaypoints ?? [] + ) { + billingHandler.stopBillingSession(with: sessionUUID) + beginNewSession(of: .activeGuidance) + } + } + } else { + beginNewSession(of: .activeGuidance) + } + } + + @MainActor + private func verifyFreeDriveBillingSession() -> UUID? { + if let sessionUUID, + let sessionType = billingHandler.sessionType(uuid: sessionUUID) + { + switch sessionType { + case .freeDrive: + billingHandler.resumeBillingSession(with: sessionUUID) + case .activeGuidance: + billingHandler.stopBillingSession(with: sessionUUID) + beginNewSession(of: .freeDrive) + return sessionUUID + } + } else { + beginNewSession(of: .freeDrive) + } + return nil + } + + // MARK: - Implementation + + private let taskManager = TaskManager() + + @MainActor + private let billingHandler: BillingHandler + + private var sessionUUID: UUID? + + private var navigator: CoreNavigator { + configuration.navigator + } + + private let configuration: Configuration + + private var rerouteController: RerouteController? + + private var privateRouteProgress: RouteProgress? + + private let locationClient: LocationClient + + @MainActor + init(configuration: Configuration) { + self.configuration = configuration + self.locationClient = configuration.locationClient + self.roadMatching = .init( + roadGraph: configuration.navigator.roadGraph, + roadObjectStore: configuration.navigator.roadObjectStore, + roadObjectMatcher: configuration.navigator.roadObjectMatcher + ) + + self._session = .init(.init(state: .idle)) + self._mapMatching = .init(nil) + self._offlineFallbacks = .init() + self._voiceInstructions = .init() + self._bannerInstructions = .init() + self._waypointsArrival = .init() + self._rerouting = .init() + self._continuousAlternatives = .init() + self._fasterRoutes = .init() + self._routeRefreshing = .init() + self._eHorizonEvents = .init() + self._errors = .init() + self._routeProgress = .init(nil) + self._navigationRoutes = .init(nil) + self.rerouteController = configuration.navigator.rerouteController + self.billingHandler = configuration.billingHandler + let statusUpdateEvents = AsyncStreamBridge(bufferingPolicy: .bufferingNewest(1)) + self.statusUpdateEvents = statusUpdateEvents + + Task.detached { [weak self] in + for await status in statusUpdateEvents { + guard let self else { return } + + taskManager.cancellableTask { + await self.update(to: status) + } + } + } + + subscribeNotifications() + subscribeLocationUpdates() + + navigator.pause() + } + + deinit { + unsubscribeNotifications() + } + + // MARK: - NavigationStatus processing + + private func updateRouteProgress(with routes: NavigationRoutes?) async { + if let routes { + let waypoints = routes.mainRoute.route.legs.enumerated() + .reduce(into: [MapboxDirections.Waypoint]()) { partialResult, element in + if element.offset == 0 { + element.element.source.map { partialResult.append($0) } + } + element.element.destination.map { partialResult.append($0) } + } + let routeProgress = RouteProgress( + navigationRoutes: routes, + waypoints: waypoints, + congestionConfiguration: configuration.congestionConfig + ) + privateRouteProgress = routeProgress + await send(RouteProgressState(routeProgress: routeProgress)) + } else { + privateRouteProgress = nil + await send(RouteProgressState?.none) + } + } + + private func update(to status: NavigationStatus) async { + guard await currentSession.state != .idle else { + await send(NavigatorErrors.UnexpectedNavigationStatus()) + Log.warning( + "Received `NavigationStatus` while not in Active Guidance or Free Drive.", + category: .navigation + ) + return + } + + guard await billingSessionIsActive() else { + Log.error( + "Received `NavigationStatus` while billing session is not running.", + category: .billing + ) + return + } + + guard !Task.isCancelled else { return } + await updateMapMatching(status: status) + + guard case .activeGuidance = await currentSession.state else { + return + } + + guard !Task.isCancelled else { return } + await send(Session(state: .activeGuidance(.init(status.routeState)))) + + guard !Task.isCancelled else { return } + await updateIndices(status: status) + await updateAlternativesPassingForkPoint(status: status) + + if let privateRouteProgress, !Task.isCancelled { + await send(RouteProgressState(routeProgress: privateRouteProgress)) + } + await handleRouteProgressUpdates(status: status) + } + + func updateMapMatching(status: NavigationStatus) async { + let snappedLocation = CLLocation(status.location) + let roadName = status.localizedRoadName() + + let localeUnit: UnitSpeed? = { + switch status.speedLimit.localeUnit { + case .kilometresPerHour: + return .kilometersPerHour + case .milesPerHour: + return .milesPerHour + @unknown default: + Log.fault("Unhandled speed limit locale unit: \(status.speedLimit.localeUnit)", category: .navigation) + return nil + } + }() + + let signStandard: SignStandard = { + switch status.speedLimit.localeSign { + case .mutcd: + return .mutcd + case .vienna: + return .viennaConvention + @unknown default: + Log.fault( + "Unknown native speed limit sign locale \(status.speedLimit.localeSign)", + category: .navigation + ) + return .viennaConvention + } + }() + + let speedLimit: Measurement? = { + if let speed = status.speedLimit.speed?.doubleValue, let localeUnit { + return Measurement(value: speed, unit: localeUnit) + } else { + return nil + } + }() + + let currentSpeedUnit: UnitSpeed = { + if let localeUnit { + return localeUnit + } else { + switch signStandard { + case .mutcd: + return .milesPerHour + case .viennaConvention: + return .kilometersPerHour + } + } + }() + + await send(MapMatchingState( + location: navigator.rawLocation ?? snappedLocation, + mapMatchingResult: MapMatchingResult(status: status), + speedLimit: SpeedLimit( + value: speedLimit, + signStandard: signStandard + ), + currentSpeed: Measurement( + value: CLLocation(status.location).speed, + unit: .metersPerSecond + ).converted(to: currentSpeedUnit), + roadName: roadName.text.isEmpty ? nil : roadName + )) + } + + private var previousArrivalWaypoint: MapboxDirections.Waypoint? + + func handleRouteProgressUpdates(status: NavigationStatus) async { + guard let privateRouteProgress else { return } + + if let newSpokenInstruction = privateRouteProgress.currentLegProgress.currentStepProgress + .currentSpokenInstruction + { + await send(SpokenInstructionState(spokenInstruction: newSpokenInstruction)) + } + + if let newVisualInstruction = privateRouteProgress.currentLegProgress.currentStepProgress + .currentVisualInstruction + { + await send(VisualInstructionState(visualInstruction: newVisualInstruction)) + } + + let legProgress = privateRouteProgress.currentLegProgress + + // We are at least at the "You will arrive" instruction + if legProgress.remainingSteps.count <= 2 { + if status.routeState == .complete { + guard previousArrivalWaypoint != legProgress.leg.destination else { + return + } + if let destination = legProgress.leg.destination { + previousArrivalWaypoint = destination + let event: any WaypointArrivalEvent = if privateRouteProgress.isFinalLeg { + WaypointArrivalStatus.Events.ToFinalDestination(destination: destination) + } else { + WaypointArrivalStatus.Events.ToWaypoint( + waypoint: destination, + legIndex: privateRouteProgress.legIndex + ) + } + await send(WaypointArrivalStatus(event: event)) + } + let advancesToNextLeg = switch configuration.multilegAdvancing { + case .automatically: + true + case .manually(let approval): + await approval(.init(arrivedLegIndex: privateRouteProgress.legIndex)) + } + guard !privateRouteProgress.isFinalLeg, advancesToNextLeg else { + return + } + switchLeg(newLegIndex: Int(status.legIndex) + 1) + } + } + } + + fileprivate func updateAlternativesPassingForkPoint(status: NavigationStatus) async { + guard var navigationRoutes = currentNavigationRoutes else { return } + + guard navigationRoutes.updateForkPointPassed(with: status) else { return } + + privateRouteProgress?.updateAlternativeRoutes(using: navigationRoutes) + await send(navigationRoutes) + let alternativesStatus = AlternativesStatus( + event: AlternativesStatus.Events.Updated( + actualAlternativeRoutes: navigationRoutes.alternativeRoutes + ) + ) + await send(alternativesStatus) + } + + func updateIndices(status: NavigationStatus) async { + if let currentNavigationRoutes { + privateRouteProgress?.updateAlternativeRoutes(using: currentNavigationRoutes) + } + privateRouteProgress?.update(using: status) + } + + // MARK: - Notifications handling + + var subscriptions = Set() + + @MainActor + private func subscribeNotifications() { + rerouteController?.delegate = self + + [ + // Navigator + (Notification.Name.navigationDidSwitchToFallbackVersion, MapboxNavigator.fallbackToOffline(_:)), + (Notification.Name.navigationDidSwitchToTargetVersion, MapboxNavigator.restoreToOnline(_:)), + (Notification.Name.navigationStatusDidChange, MapboxNavigator.navigationStatusDidChange(_:)), + ( + Notification.Name.navigatorDidChangeAlternativeRoutes, + MapboxNavigator.navigatorDidChangeAlternativeRoutes(_:) + ), + ( + Notification.Name.navigatorDidFailToChangeAlternativeRoutes, + MapboxNavigator.navigatorDidFailToChangeAlternativeRoutes(_:) + ), + ( + Notification.Name.navigatorWantsSwitchToCoincideOnlineRoute, + MapboxNavigator.navigatorWantsSwitchToCoincideOnlineRoute(_:) + ), + (Notification.Name.routeRefreshDidUpdateAnnotations, MapboxNavigator.didRefreshAnnotations(_:)), + (Notification.Name.routeRefreshDidFailRefresh, MapboxNavigator.didFailToRefreshAnnotations(_:)), + // EH + ( + Notification.Name.electronicHorizonDidUpdatePosition, + MapboxNavigator.didUpdateElectronicHorizonPosition(_:) + ), + ( + Notification.Name.electronicHorizonDidEnterRoadObject, + MapboxNavigator.didEnterElectronicHorizonRoadObject(_:) + ), + ( + Notification.Name.electronicHorizonDidExitRoadObject, + MapboxNavigator.didExitElectronicHorizonRoadObject(_:) + ), + ( + Notification.Name.electronicHorizonDidPassRoadObject, + MapboxNavigator.didPassElectronicHorizonRoadObject(_:) + ), + ] + .forEach(subscribe(to:)) + + subscribeFasterRouteController() + } + + func disableTrackingBackgroundLocationIfNeeded() { + Task { + guard configuration.disableBackgroundTrackingLocation, + await currentSession.state == .freeDrive(.active) + else { + return + } + + await pauseFreeDrive() + await send(Session(state: .freeDrive(.active))) + } + } + + func restoreTrackingLocationIfNeeded() { + Task { + guard configuration.disableBackgroundTrackingLocation, + await currentSession.state == .freeDrive(.active) + else { + return + } + + await startFreeDrive() + } + } + + private func subscribeLocationUpdates() { + locationClient.locations + .receive(on: DispatchQueue.main) + .sink { [weak self] location in + guard let self else { return } + Task { @MainActor in + guard self.billingSessionIsActive() else { + Log.warning( + "Received location update while billing session is not running.", + category: .billing + ) + return + } + + self.navigator.updateLocation(location, completion: { _ in }) + } + }.store(in: &subscriptions) + } + + @MainActor + private func subscribeFasterRouteController() { + guard let fasterRouteController = configuration.fasterRouteController else { return } + + routeProgress + .compactMap { $0 } + .sink { currentRouteProgress in + fasterRouteController.checkForFasterRoute( + from: currentRouteProgress.routeProgress + ) + } + .store(in: &subscriptions) + + navigationRoutes + .sink { navigationRoutes in + fasterRouteController.navigationRoute = navigationRoutes?.mainRoute + } + .store(in: &subscriptions) + + mapMatching + .compactMap { $0 } + .sink { mapMatch in + fasterRouteController.currentLocation = mapMatch.enhancedLocation + } + .store(in: &subscriptions) + + rerouting + .sink { + fasterRouteController.isRerouting = $0.event is ReroutingStatus.Events.FetchingRoute + } + .store(in: &subscriptions) + + fasterRouteController.fasterRoutes + .receive(on: DispatchQueue.main) + .sink { [weak self] fasterRoutes in + Task { [weak self] in + self?.send( + FasterRoutesStatus( + event: FasterRoutesStatus.Events.Detected() + ) + ) + self?.taskManager.cancellableTask { [weak self] in + guard !Task.isCancelled else { return } + await self?.setRoutes( + navigationRoutes: fasterRoutes, + startLegIndex: 0, + reason: .fasterRoute + ) + } + } + } + .store(in: &subscriptions) + } + + private func subscribe( + to item: (name: Notification.Name, sink: (MapboxNavigator) -> (Notification) -> Void) + ) { + NotificationCenter.default + .publisher(for: item.name) + .sink { [weak self] notification in + self.map { item.sink($0)(notification) } + } + .store(in: &subscriptions) + } + + private func unsubscribeNotifications() { + rerouteController?.delegate = nil + subscriptions.removeAll() + } + + func fallbackToOffline(_ notification: Notification) { + Task { @MainActor in + rerouteController = configuration.navigator.rerouteController + rerouteController?.delegate = self + + guard let navigationRoutes = self.currentNavigationRoutes, + let privateRouteProgress else { return } + taskManager.cancellableTask { [self] in + guard !Task.isCancelled else { return } + await setRoutes( + navigationRoutes: navigationRoutes, + startLegIndex: privateRouteProgress.legIndex, + reason: .fallbackToOffline + ) + } + } + } + + func restoreToOnline(_ notification: Notification) { + Task { @MainActor in + rerouteController = configuration.navigator.rerouteController + rerouteController?.delegate = self + + guard let navigationRoutes = self.currentNavigationRoutes, + let privateRouteProgress else { return } + taskManager.cancellableTask { [self] in + guard !Task.isCancelled else { return } + await setRoutes( + navigationRoutes: navigationRoutes, + startLegIndex: privateRouteProgress.legIndex, + reason: .restoreToOnline + ) + } + } + } + + private func navigationStatusDidChange(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let status = userInfo[NativeNavigator.NotificationUserInfoKey.statusKey] as? NavigationStatus + else { return } + statusUpdateEvents.yield(status) + } + + private func navigatorDidChangeAlternativeRoutes(_ notification: Notification) { + guard let alternativesAcceptionPolicy = configuration.alternativesAcceptionPolicy, + let mainRoute = currentNavigationRoutes?.mainRoute, + let userInfo = notification.userInfo, + let alternatives = + userInfo[NativeNavigator.NotificationUserInfoKey.alternativesListKey] as? [RouteAlternative] + else { + return + } + + Task { @MainActor in + navigator.setAlternativeRoutes(with: alternatives.map(\.route)) + { [weak self] result /* Result<[RouteAlternative], Error> */ in + guard let self else { return } + + Task { + switch result { + case .success(let routeAlternatives): + let alternativeRoutes = await AlternativeRoute.fromNative( + alternativeRoutes: routeAlternatives, + relateveTo: mainRoute + ) + + guard var navigationRoutes = self.currentNavigationRoutes else { return } + navigationRoutes.allAlternativeRoutesWithIgnored = alternativeRoutes + .filter { alternativeRoute in + if alternativesAcceptionPolicy.contains(.unfiltered) { + return true + } else { + if alternativesAcceptionPolicy.contains(.fasterRoutes), + alternativeRoute.expectedTravelTimeDelta < 0 + { + return true + } + if alternativesAcceptionPolicy.contains(.shorterRoutes), + alternativeRoute.distanceDelta < 0 + { + return true + } + } + return false + } + if let status = self.navigator.mostRecentNavigationStatus { + navigationRoutes.updateForkPointPassed(with: status) + } + await self.send(navigationRoutes) + await self + .send( + AlternativesStatus( + event: AlternativesStatus.Events.Updated( + actualAlternativeRoutes: navigationRoutes.alternativeRoutes + ) + ) + ) + case .failure(let updateError): + Log.warning( + "Failed to update alternative routes, error: \(updateError)", + category: .navigation + ) + let error = NavigatorErrors.FailedToUpdateAlternativeRoutes( + localizedDescription: updateError.localizedDescription + ) + await self.send(error) + } + } + } + } + } + + private func navigatorDidFailToChangeAlternativeRoutes(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let message = userInfo[NativeNavigator.NotificationUserInfoKey.messageKey] as? String + else { + return + } + Log.error("Failed to change alternative routes: \(message)", category: .navigation) + Task { @MainActor in + send(NavigatorErrors.FailedToUpdateAlternativeRoutes(localizedDescription: message)) + } + } + + private func navigatorWantsSwitchToCoincideOnlineRoute(_ notification: Notification) { + guard configuration.prefersOnlineRoute, + let userInfo = notification.userInfo, + let onlineRoute = + userInfo[NativeNavigator.NotificationUserInfoKey.coincideOnlineRouteKey] as? RouteInterface + else { + return + } + + Task { + guard let route = await NavigationRoute(nativeRoute: onlineRoute) else { + return + } + let navigationRoutes = await NavigationRoutes( + mainRoute: route, + alternativeRoutes: [] + ) + + taskManager.cancellableTask { [self] in + guard !Task.isCancelled else { return } + await setRoutes( + navigationRoutes: navigationRoutes, + startLegIndex: 0, + reason: .restoreToOnline + ) + } + } + } + + func didUpdateElectronicHorizonPosition(_ notification: Notification) { + guard let position = notification.userInfo?[RoadGraph.NotificationUserInfoKey.positionKey] as? RoadGraph + .Position, + let startingEdge = notification.userInfo?[RoadGraph.NotificationUserInfoKey.treeKey] as? RoadGraph.Edge, + let updatesMostProbablePath = notification + .userInfo?[RoadGraph.NotificationUserInfoKey.updatesMostProbablePathKey] as? Bool, + let distancesByRoadObject = notification + .userInfo?[RoadGraph.NotificationUserInfoKey.distancesByRoadObjectKey] as? [DistancedRoadObject] + else { + return + } + + let event = EHorizonStatus.Events.PositionUpdated( + position: position, + startingEdge: startingEdge, + updatesMostProbablePath: updatesMostProbablePath, + distances: distancesByRoadObject + ) + Task { @MainActor in + send(EHorizonStatus(event: event)) + } + } + + func didEnterElectronicHorizonRoadObject(_ notification: Notification) { + guard let objectId = notification + .userInfo?[RoadGraph.NotificationUserInfoKey.roadObjectIdentifierKey] as? RoadObject.Identifier, + let hasEnteredFromStart = notification + .userInfo?[RoadGraph.NotificationUserInfoKey.didTransitionAtEndpointKey] as? Bool + else { + return + } + let event = EHorizonStatus.Events.RoadObjectEntered( + roadObjectId: objectId, + enteredFromStart: hasEnteredFromStart + ) + + Task { @MainActor in + send(EHorizonStatus(event: event)) + } + } + + func didExitElectronicHorizonRoadObject(_ notification: Notification) { + guard let objectId = notification + .userInfo?[RoadGraph.NotificationUserInfoKey.roadObjectIdentifierKey] as? RoadObject.Identifier, + let hasExitedFromEnd = notification + .userInfo?[RoadGraph.NotificationUserInfoKey.didTransitionAtEndpointKey] as? Bool + else { + return + } + let event = EHorizonStatus.Events.RoadObjectExited( + roadObjectId: objectId, + exitedFromEnd: hasExitedFromEnd + ) + Task { @MainActor in + send(EHorizonStatus(event: event)) + } + } + + func didPassElectronicHorizonRoadObject(_ notification: Notification) { + guard let objectId = notification + .userInfo?[RoadGraph.NotificationUserInfoKey.roadObjectIdentifierKey] as? RoadObject.Identifier + else { + return + } + let event = EHorizonStatus.Events.RoadObjectPassed(roadObjectId: objectId) + Task { @MainActor in + send(EHorizonStatus(event: event)) + } + } + + func didRefreshAnnotations(_ notification: Notification) { + guard let refreshRouteResult = notification + .userInfo?[NativeNavigator.NotificationUserInfoKey.refreshedRoutesResultKey] as? RouteRefreshResult, + let legIndex = notification.userInfo?[NativeNavigator.NotificationUserInfoKey.legIndexKey] as? UInt32, + let currentNavigationRoutes + else { + return + } + + Task { + guard case .activeGuidance = await currentSession.state else { + return + } + + var newMainRoute = currentNavigationRoutes.mainRoute + let isMainRouteUpdate = refreshRouteResult.updatedRoute.getRouteId() == + currentNavigationRoutes.mainRoute.routeId.rawValue + if isMainRouteUpdate { + guard let updatedMainRoute = await NavigationRoute(nativeRoute: refreshRouteResult.updatedRoute) + else { return } + newMainRoute = updatedMainRoute + } + let event = RefreshingStatus.Events.Refreshing() + await send(RefreshingStatus(event: event)) + + var refreshedNavigationRoutes = await NavigationRoutes( + mainRoute: newMainRoute, + alternativeRoutes: await AlternativeRoute.fromNative( + alternativeRoutes: refreshRouteResult.alternativeRoutes, + relateveTo: newMainRoute + ) + ) + if let status = self.navigator.mostRecentNavigationStatus { + refreshedNavigationRoutes.updateForkPointPassed(with: status) + } + self.privateRouteProgress = privateRouteProgress?.refreshingRoute( + with: refreshedNavigationRoutes, + legIndex: Int(legIndex), + legShapeIndex: 0, // TODO: NN should provide this value in `MBNNRouteRefreshObserver` + congestionConfiguration: configuration.congestionConfig + ) + await self.send(refreshedNavigationRoutes) + + if let privateRouteProgress { + await send(RouteProgressState(routeProgress: privateRouteProgress)) + } + let endEvent = RefreshingStatus.Events.Refreshed() + await send(RefreshingStatus(event: endEvent)) + } + } + + func didFailToRefreshAnnotations(_ notification: Notification) { + guard let refreshRouteFailure = notification + .userInfo?[NativeNavigator.NotificationUserInfoKey.refreshRequestErrorKey] as? RouteRefreshError, + refreshRouteFailure.refreshTtl == 0, + let currentNavigationRoutes + else { + return + } + + Task { + await send( + RefreshingStatus( + event: RefreshingStatus.Events.Invalidated( + navigationRoutes: currentNavigationRoutes + ) + ) + ) + } + } +} + +// MARK: - ReroutingControllerDelegate + +extension MapboxNavigator: ReroutingControllerDelegate { + func rerouteControllerWantsSwitchToAlternative( + _ rerouteController: RerouteController, + route: RouteInterface, + legIndex: Int + ) { + Task { + guard let navigationRoute = await NavigationRoute(nativeRoute: route) else { + return + } + + taskManager.cancellableTask { [self] in + guard !Task.isCancelled else { return } + await setRoutes( + navigationRoutes: NavigationRoutes( + mainRoute: navigationRoute, + alternativeRoutes: [] + ), + startLegIndex: legIndex, + reason: .alternatives + ) + } + } + } + + func rerouteControllerDidDetectReroute(_ rerouteController: RerouteController) { + Log.debug("Reroute was detected.", category: .navigation) + Task { @MainActor in + send( + ReroutingStatus( + event: ReroutingStatus.Events.FetchingRoute() + ) + ) + } + } + + func rerouteControllerDidRecieveReroute(_ rerouteController: RerouteController, routesData: RoutesData) { + Log.debug( + "Reroute was fetched with primary route id '\(routesData.primaryRoute().getRouteId())' and \(routesData.alternativeRoutes().count) alternative route(s).", + category: .navigation + ) + Task { + guard let navigationRoutes = try? await NavigationRoutes(routesData: routesData) else { + Log.error( + "Reroute was fetched but could not convert it to `NavigationRoutes`.", + category: .navigation + ) + return + } + taskManager.cancellableTask { [self] in + guard !Task.isCancelled else { return } + await setRoutes( + navigationRoutes: navigationRoutes, + startLegIndex: 0, + reason: .reroute + ) + } + } + } + + func rerouteControllerDidCancelReroute(_ rerouteController: RerouteController) { + Log.warning("Reroute was cancelled.", category: .navigation) + Task { @MainActor in + send( + ReroutingStatus( + event: ReroutingStatus.Events.Interrupted() + ) + ) + send(NavigatorErrors.InterruptedReroute(underlyingError: nil)) + } + } + + func rerouteControllerDidFailToReroute(_ rerouteController: RerouteController, with error: DirectionsError) { + Log.error("Failed to reroute, error: \(error)", category: .navigation) + Task { @MainActor in + send( + ReroutingStatus( + event: ReroutingStatus.Events.Failed(error: error) + ) + ) + send(NavigatorErrors.InterruptedReroute(underlyingError: error)) + } + } +} + +extension MapboxNavigator { + @MainActor + private func send(_ details: NavigationRoutes?) { + if details == nil { + previousArrivalWaypoint = nil + } + _navigationRoutes.emit(details) + } + + @MainActor + private func send(_ details: Session) { + _session.emit(details) + } + + @MainActor + private func send(_ details: MapMatchingState) { + _mapMatching.emit(details) + } + + @MainActor + private func send(_ details: RouteProgressState?) { + _routeProgress.emit(details) + } + + @MainActor + private func send(_ details: FallbackToTilesState) { + _offlineFallbacks.emit(details) + } + + @MainActor + private func send(_ details: SpokenInstructionState) { + _voiceInstructions.emit(details) + } + + @MainActor + private func send(_ details: VisualInstructionState) { + _bannerInstructions.emit(details) + } + + @MainActor + private func send(_ details: WaypointArrivalStatus) { + _waypointsArrival.emit(details) + } + + @MainActor + private func send(_ details: ReroutingStatus) { + _rerouting.emit(details) + } + + @MainActor + private func send(_ details: AlternativesStatus) { + _continuousAlternatives.emit(details) + } + + @MainActor + private func send(_ details: FasterRoutesStatus) { + _fasterRoutes.emit(details) + } + + @MainActor + private func send(_ details: RefreshingStatus) { + _routeRefreshing.emit(details) + } + + @MainActor + private func send(_ details: NavigatorError) { + _errors.emit(details) + } + + @MainActor + private func send(_ details: EHorizonStatus) { + _eHorizonEvents.emit(details) + } +} + +// MARK: - TaskManager + +extension MapboxNavigator { + fileprivate final class TaskManager: Sendable { + private let tasksInFlight_ = UnfairLocked([String: Task]()) + func cancellableTask( + id: String = #function, + operation: @Sendable @escaping () async throws -> Void + ) rethrows { + Task { + defer { + _ = tasksInFlight_.mutate { + $0.removeValue(forKey: id) + } + } + + guard !barrier.read() else { return } + let task = Task { try await operation() } + tasksInFlight_.mutate { + $0[id]?.cancel() + $0[id] = task + } + _ = try await task.value + } + } + + func cancelTasks() { + tasksInFlight_.mutate { + $0.forEach { + $0.value.cancel() + } + $0.removeAll() + } + } + + private let barrier: UnfairLocked = .init(false) + + @MainActor + func withBarrier(_ operation: () -> Void) { + barrier.update(true) + cancelTasks() + operation() + barrier.update(false) + } + } +} + +extension MapboxNavigator.SetRouteReason { + var navNativeValue: MapboxNavigationNative.SetRoutesReason { + switch self { + case .newRoute: + return .newRoute + case .alternatives: + return .alternative + case .reroute: + return .reroute + case .fallbackToOffline: + return .fallbackToOffline + case .restoreToOnline: + return .restoreToOnline + case .fasterRoute: + return .fastestRoute + } + } +} + +extension NavigationRoutes { + @discardableResult + mutating func updateForkPointPassed(with status: NavigationStatus) -> Bool { + let newPassedForkPointRouteIds = Set( + status.alternativeRouteIndices + .compactMap { $0.isForkPointPassed ? $0.routeId : nil } + ) + let oldPassedForkPointRouteIds = Set( + allAlternativeRoutesWithIgnored + .compactMap { $0.isForkPointPassed ? $0.routeId.rawValue : nil } + ) + guard newPassedForkPointRouteIds != oldPassedForkPointRouteIds else { return false } + + for (index, route) in allAlternativeRoutesWithIgnored.enumerated() { + allAlternativeRoutesWithIgnored[index].isForkPointPassed = + newPassedForkPointRouteIds.contains(route.routeId.rawValue) + } + return true + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationLocationManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationLocationManager.swift new file mode 100644 index 000000000..c7d56f650 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationLocationManager.swift @@ -0,0 +1,44 @@ +import CoreLocation +import Foundation +#if os(iOS) +import UIKit +#endif + +/// ``NavigationLocationManager`` is the base location manager which handles permissions and background modes. +open class NavigationLocationManager: CLLocationManager { + @MainActor + override public init() { + super.init() + + requestWhenInUseAuthorization() + + if Bundle.main.backgroundModes.contains("location") { + allowsBackgroundLocationUpdates = true + } + + delegate = self + } + + /// Indicates whether the location manager is providing simulated locations. + open var simulatesLocation: Bool = false + + public weak var locationDelegate: NavigationLocationManagerDelegate? = nil +} + +public protocol NavigationLocationManagerDelegate: AnyObject { + func navigationLocationManager( + _ locationManager: NavigationLocationManager, + didReceiveNewLocation location: CLLocation + ) +} + +extension NavigationLocationManager: CLLocationManagerDelegate { + open func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + if let location = locations.last { + locationDelegate?.navigationLocationManager( + self, + didReceiveNewLocation: location + ) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationRoutes.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationRoutes.swift new file mode 100644 index 000000000..d922d0b58 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationRoutes.swift @@ -0,0 +1,420 @@ +import _MapboxNavigationHelpers +import Foundation +import MapboxDirections +@preconcurrency import MapboxNavigationNative +import Turf + +/// Contains a selection of ``NavigationRoute`` and it's related ``AlternativeRoute``'s, which can be sued for +/// navigation. +public struct NavigationRoutes: Equatable, @unchecked Sendable { + /// A route choosed to navigate on. + public internal(set) var mainRoute: NavigationRoute + /// Suggested alternative routes. + /// + /// To select one of the alterntives as a main route, see ``selectingAlternativeRoute(at:)`` and + /// ``selecting(alternativeRoute:)`` methods. + public var alternativeRoutes: [AlternativeRoute] { + allAlternativeRoutesWithIgnored.filter { !$0.isForkPointPassed } + } + + /// A list of ``Waypoint``s visited along the routes. + public internal(set) var waypoints: [Waypoint] + /// A deadline after which the routes from this `RouteResponse` are eligable for refreshing. + /// + /// `nil` value indicates that route refreshing is not available for related routes. + public internal(set) var refreshInvalidationDate: Date? + /// Contains a map of `JSONObject`'s which were appended in the original route response, but are not recognized by + /// the SDK. + public internal(set) var foreignMembers: JSONObject = [:] + + var allAlternativeRoutesWithIgnored: [AlternativeRoute] + + var isCustomExternalRoute: Bool { + mainRoute.nativeRoute.getRouterOrigin() == .customExternal + } + + init(routesData: RoutesData) async throws { + let routeResponse = try await routesData.primaryRoute().convertToDirectionsRouteResponse() + try self.init(routesData: routesData, routeResponse: routeResponse) + } + + private init(routesData: RoutesData, routeResponse: RouteResponse) throws { + guard let routes = routeResponse.routes else { + Log.error("Unable to get routes", category: .navigation) + throw NavigationRoutesError.emptyRoutes + } + + guard routes.count == routesData.alternativeRoutes().count + 1 else { + Log.error("Routes mismatched", category: .navigation) + throw NavigationRoutesError.incorrectRoutesNumber + } + + let mainRoute = routes[Int(routesData.primaryRoute().getRouteIndex())] + + var alternativeRoutes = [AlternativeRoute]() + for routeAlternative in routesData.alternativeRoutes() { + guard let alternativeRoute = AlternativeRoute( + mainRoute: mainRoute, + alternativeRoute: routes[Int(routeAlternative.route.getRouteIndex())], + nativeRouteAlternative: routeAlternative + ) else { + Log.error("Unable to convert alternative route with id: \(routeAlternative.id)", category: .navigation) + continue + } + + alternativeRoutes.append(alternativeRoute) + } + + self.mainRoute = NavigationRoute(route: mainRoute, nativeRoute: routesData.primaryRoute()) + self.allAlternativeRoutesWithIgnored = alternativeRoutes + self.waypoints = routeResponse.waypoints ?? [] + self.refreshInvalidationDate = routeResponse.refreshInvalidationDate + self.foreignMembers = routeResponse.foreignMembers + } + + init(mainRoute: NavigationRoute, alternativeRoutes: [AlternativeRoute]) async { + self.mainRoute = mainRoute + self.allAlternativeRoutesWithIgnored = alternativeRoutes + + let response = try? await mainRoute.nativeRoute.convertToDirectionsRouteResponse() + self.waypoints = response?.waypoints ?? [] + self.refreshInvalidationDate = response?.refreshInvalidationDate + if let foreignMembers = response?.foreignMembers { + self.foreignMembers = foreignMembers + } + } + + @_spi(MapboxInternal) + public init(routeResponse: RouteResponse, routeIndex: Int, responseOrigin: RouterOrigin) async throws { + let options = NavigationRoutes.validatedRouteOptions(options: routeResponse.options) + + let encoder = JSONEncoder() + encoder.userInfo[.options] = options + let routeData = try encoder.encode(routeResponse) + + let routeRequest = Directions.url(forCalculating: options, credentials: routeResponse.credentials) + .absoluteString + + let parsedRoutes = RouteParser.parseDirectionsResponse( + forResponseDataRef: .init(data: routeData), + request: routeRequest, + routeOrigin: responseOrigin + ) + if parsedRoutes.isValue(), + var routes = parsedRoutes.value as? [RouteInterface], + routes.indices.contains(routeIndex) + { + let routesData = RouteParser.createRoutesData( + forPrimaryRoute: routes.remove(at: routeIndex), + alternativeRoutes: routes + ) + let navigationRoutes = try NavigationRoutes(routesData: routesData, routeResponse: routeResponse) + self = navigationRoutes + self.waypoints = routeResponse.waypoints ?? [] + self.refreshInvalidationDate = routeResponse.refreshInvalidationDate + self.foreignMembers = routeResponse.foreignMembers + } else if parsedRoutes.isError(), + let error = parsedRoutes.error + { + Log.error("Failed to parse routes with error: \(error)", category: .navigation) + throw NavigationRoutesError.responseParsingError(description: error as String) + } else { + Log.error("Unexpected error during routes parsing.", category: .navigation) + throw NavigationRoutesError.unknownError + } + } + + func asRoutesData() -> RoutesData { + return RouteParser.createRoutesData( + forPrimaryRoute: mainRoute.nativeRoute, + alternativeRoutes: alternativeRoutes.map(\.nativeRoute) + ) + } + + func selectingMostSimilar(to route: Route) async -> NavigationRoutes { + let target = route.description + + var candidates = [mainRoute.route] + candidates.append(contentsOf: alternativeRoutes.map(\.route)) + + guard let bestCandidate = candidates.map({ + (route: $0, editDistance: $0.description.minimumEditDistance(to: target)) + }).enumerated().min(by: { $0.element.editDistance < $1.element.editDistance }) else { return self } + + // If the most similar route is still more than 50% different from the original route, + // we fallback to the fastest route which index is 0. + let totalLength = Double(bestCandidate.element.route.description.count + target.description.count) + guard totalLength > 0 else { return self } + let differenceScore = Double(bestCandidate.element.editDistance) / totalLength + // Comparing to 0.25 as for "replacing the half of the string", since we add target and candidate lengths + // together + // Algorithm proposal: https://github.com/mapbox/mapbox-navigation-ios/pull/3664#discussion_r772194977 + guard differenceScore < 0.25 else { return self } + + if bestCandidate.offset > 0 { + return await selectingAlternativeRoute(at: bestCandidate.offset - 1) ?? self + } else { + return self + } + } + + /// Returns a new ``NavigationRoutes`` instance, wich has corresponding ``AlternativeRoute`` set as the main one. + /// + /// This operation requires re-parsing entire routes data, because all alternative's relative stats will not remain + /// the same after changing the ``mainRoute``. + /// + /// - parameter index: Index in ``alternativeRoutes`` array to assign as a main route. + /// - returns: New ``NavigationRoutes`` instance, with new `alternativeRoute` set as the main one, or `nil` if the + /// `index` is out of bounds.. + public func selectingAlternativeRoute(at index: Int) async -> NavigationRoutes? { + guard self.alternativeRoutes.indices.contains(index) else { + return nil + } + var alternativeRoutes = alternativeRoutes + + let alternativeRoute = alternativeRoutes.remove(at: index) + + let routesData = RouteParser.createRoutesData( + forPrimaryRoute: alternativeRoute.nativeRoute, + alternativeRoutes: alternativeRoutes.map(\.nativeRoute) + [mainRoute.nativeRoute] + ) + + let newMainRoute = NavigationRoute(route: alternativeRoute.route, nativeRoute: alternativeRoute.nativeRoute) + + var newAlternativeRoutes = alternativeRoutes.compactMap { oldAlternative -> AlternativeRoute? in + guard let nativeRouteAlternative = routesData.alternativeRoutes() + .first(where: { $0.route.getRouteId() == oldAlternative.routeId.rawValue }) + else { + Log.warning( + "Unable to create an alternative route for \(oldAlternative.routeId.rawValue)", + category: .navigation + ) + return nil + } + return AlternativeRoute( + mainRoute: newMainRoute.route, + alternativeRoute: oldAlternative.route, + nativeRouteAlternative: nativeRouteAlternative + ) + } + + if let nativeRouteAlternative = routesData.alternativeRoutes() + .first(where: { $0.route.getRouteId() == mainRoute.routeId.rawValue }), + let newAlternativeRoute = AlternativeRoute( + mainRoute: newMainRoute.route, + alternativeRoute: mainRoute.route, + nativeRouteAlternative: nativeRouteAlternative + ) + { + newAlternativeRoutes.append(newAlternativeRoute) + } else { + Log.warning( + "Unable to create an alternative route: \(mainRoute.routeId.rawValue) for a new main route: \(alternativeRoute.routeId.rawValue)", + category: .navigation + ) + } + + return await .init(mainRoute: newMainRoute, alternativeRoutes: newAlternativeRoutes) + } + + /// Returns a new ``NavigationRoutes`` instance, wich has corresponding ``AlternativeRoute`` set as the main one. + /// + /// This operation requires re-parsing entire routes data, because all alternative's relative stats will not remain + /// the same after changing the ``mainRoute``. + /// + /// - parameter alternativeRoute: An ``AlternativeRoute`` to assign as main. + /// - returns: New ``NavigationRoutes`` instance, with `alternativeRoute` set as the main one, or `nil` if current + /// instance does not contain this alternative. + public func selecting(alternativeRoute: AlternativeRoute) async -> NavigationRoutes? { + guard let index = alternativeRoutes.firstIndex(where: { $0 == alternativeRoute }) else { + return nil + } + return await selectingAlternativeRoute(at: index) + } + + static func validatedRouteOptions(options: ResponseOptions) -> RouteOptions { + switch options { + case .match(let matchOptions): + return RouteOptions(matchOptions: matchOptions) + case .route(let options): + return options + } + } + + /// A convenience method to get a list of all included `Route`s, optionally filtering it in the process. + /// + /// - parameter isIncluded: A callback, used to filter the routes. + /// - returns: A list of all included routes, filtered by `isIncluded` rule. + public func allRoutes(_ isIncluded: (Route) -> Bool = { _ in true }) -> [Route] { + var routes: [Route] = [] + if isIncluded(mainRoute.route) { + routes.append(mainRoute.route) + } + routes.append(contentsOf: alternativeRoutes.lazy.map(\.route).filter(isIncluded)) + return routes + } + + /// Convenience method to comare routes set with another ``NavigationRoutes`` instance. + /// + /// - note: The comparison is done by ``NavigationRoute/routeId``. + /// + /// - parameter otherRoutes: A ``NavigationRoutes`` instance against which to compare. + /// - returns: `true` if `otherRoutes` contains exactly the same collection of routes, `false` - otherwise. + public func containsSameRoutes(as otherRoutes: NavigationRoutes) -> Bool { + let currentRouteIds = Set(alternativeRoutes.map(\.routeId) + [mainRoute.routeId]) + let newRouteIds = Set(otherRoutes.alternativeRoutes.map(\.routeId) + [otherRoutes.mainRoute.routeId]) + return currentRouteIds == newRouteIds + } +} + +/// Wraps a route object used across the Navigation SDK. +public struct NavigationRoute: Sendable { + /// // A `Route` object that the current navigation route represents. + public let route: Route + /// Unique route id. + public let routeId: RouteId + + public let nativeRoute: RouteInterface + + public init?(nativeRoute: RouteInterface) async { + self.nativeRoute = nativeRoute + self.routeId = .init(rawValue: nativeRoute.getRouteId()) + + guard let route = try? await nativeRoute.convertToDirectionsRoute() else { + return nil + } + + self.route = route + } + + init(route: Route, nativeRoute: RouteInterface) { + self.nativeRoute = nativeRoute + self.route = route + self.routeId = .init(rawValue: nativeRoute.getRouteId()) + } + + private let _routeOptions: NSLocked<(initialized: Bool, options: RouteOptions?)> = .init((false, nil)) + public var routeOptions: RouteOptions? { + _routeOptions.mutate { state in + if state.initialized { + return state.options + } else { + state.initialized = true + if let newOptions = getRouteOptions() { + state.options = newOptions + return newOptions + } else { + return nil + } + } + } + } + + private func getRouteOptions() -> RouteOptions? { + guard let url = URL(string: nativeRoute.getRequestUri()) else { + return nil + } + + return RouteOptions(url: url) + } +} + +extension NavigationRoute: Equatable { + public static func == (lhs: NavigationRoute, rhs: NavigationRoute) -> Bool { + return lhs.routeId == rhs.routeId && + lhs.route == rhs.route + } +} + +extension RouteInterface { + func convertToDirectionsRouteResponse() async throws -> RouteResponse { + guard let requestURL = URL(string: getRequestUri()), + let routeOptions = RouteOptions(url: requestURL) + else { + Log.error( + "Couldn't extract response and request data to parse `RouteInterface` into `RouteResponse`", + category: .navigation + ) + throw NavigationRoutesError.noRequestData + } + + let credentials = Credentials(requestURL: requestURL) + let decoder = JSONDecoder() + decoder.userInfo[.options] = routeOptions + decoder.userInfo[.credentials] = credentials + + do { + let ref = getResponseJsonRef() + return try decoder.decode(RouteResponse.self, from: ref.data) + } catch { + Log.error( + "Couldn't parse `RouteInterface` into `RouteResponse` with error: \(error)", + category: .navigation + ) + throw NavigationRoutesError.encodingError(underlyingError: error) + } + } + + func convertToDirectionsRoute() async throws -> Route { + do { + guard let routes = try await convertToDirectionsRouteResponse().routes else { + Log.error("Converting to directions route yielded no routes.", category: .navigation) + throw NavigationRoutesError.emptyRoutes + } + guard routes.count > getRouteIndex() else { + Log.error( + "Converting to directions route yielded incorrect number of routes (expected at least \(getRouteIndex() + 1) but have \(routes.count).", + category: .navigation + ) + throw NavigationRoutesError.incorrectRoutesNumber + } + return routes[Int(getRouteIndex())] + } catch { + Log.error( + "Parsing `RouteInterface` into `Route` resulted in no routes", + category: .navigation + ) + throw error + } + } +} + +public struct RouteId: Hashable, Sendable, Codable, CustomStringConvertible { + var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.rawValue = try container.decode(String.self) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } + + public var description: String { + "RouteId(\(rawValue)" + } +} + +/// The error describing a possible cause of failing to instantiate the ``NavigationRoutes`` object. +public enum NavigationRoutesError: Error { + /// Could not correctly encode provided data into a valid JSON. + /// + /// See the associated error for more details. + case encodingError(underlyingError: Error?) + /// Failed to compose routes object(s) from the JSON representation. + case responseParsingError(description: String) + /// Could not extract route request parameters from the JSON representation. + case noRequestData + /// Routes parsing resulted in an empty routes list. + case emptyRoutes + /// The number of decoded routes does not match the expected amount + case incorrectRoutesNumber + /// An unexpected error occurred during parsing. + case unknownError +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Navigator.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Navigator.swift new file mode 100644 index 000000000..1d3afd0b0 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Navigator.swift @@ -0,0 +1,403 @@ +import Combine +import CoreLocation +import MapboxDirections +import MapboxNavigationNative + +// MARK: - NavigationEvent + +/// The base for all ``MapboxNavigation`` events. +public protocol NavigationEvent: Equatable, Sendable {} +extension NavigationEvent { + fileprivate func compare(to other: any NavigationEvent) -> Bool { + guard let other = other as? Self else { + return false + } + return self == other + } +} + +// MARK: - SessionState + +/// Navigation session details. +public struct Session: Equatable, Sendable { + /// Current session state. + public let state: State + + /// Describes possible navigation states. + public enum State: Equatable, Sendable { + /// The navigator is idle and is not tracking user location. + case idle + /// The navigator observes user location and matches it to the road network. + case freeDrive(FreeDriveState) // MBNNRouteStateInvalid * + /// The navigator tracks user progress along the given route. + case activeGuidance(ActiveGuidanceState) + + /// Flags if navigator is currently active. + public var isTripSessionActive: Bool { + return self != .idle + } + + /// Describes possible Free Drive states + public enum FreeDriveState: Sendable { + /// Free drive is paused. + /// + /// The navigator does not currently tracks user location, but can be resumed any time. + /// Unlike switching to the ``Session/State-swift.enum/idle`` state, pausing the Free drive does not + /// interrupt the navigation session. + case paused + /// The navigator observes user location and matches it to the road network. + /// + /// Unlike switching to the ``Session/State-swift.enum/idle`` state, pausing the Free drive does not + /// interrupt the navigation session. + case active + } + + /// Describes possible Active Guidance states. + public enum ActiveGuidanceState: Sendable { + /// Initial state when starting a new route. + case initialized + /// The Navigation process is nominal. + /// + /// The navigator tracks user position and progress. + case tracking // MBNNRouteStateTracking + /// The navigator detected user went off the route. + case offRoute // MBNNRouteStateUncertain * + /// The navigator experiences troubles determining it's state. + /// + /// This may be signaled when navigator is judjing if user is still on the route or is wandering off, or + /// when GPS signal quality has dropped, or due to some other technical conditions. + /// Unless `offRoute` is reported - it is still treated as user progressing the route. + case uncertain // MBNNRouteStateInitialized + MBNNRouteStateUncertain + MBNNRouteStateInvalid(?) + /// The user has arrived to the final destination. + case complete // MBNNRouteStateComplete + + init(_ routeState: RouteState) { + switch routeState { + case .invalid, .uncertain: + self = .uncertain + case .initialized: + self = .initialized + case .tracking: + self = .tracking + case .complete: + self = .complete + case .offRoute: + self = .offRoute + @unknown default: + self = .uncertain + } + } + } + } +} + +// MARK: - RouteProgressState + +/// Route progress update event details. +public struct RouteProgressState: Sendable { + /// Actual ``RouteProgress``. + public let routeProgress: RouteProgress +} + +// MARK: - MapMatchingState + +/// Map matching update event details. +public struct MapMatchingState: Equatable, @unchecked Sendable { + /// Current user raw location. + public let location: CLLocation + /// Current user matched location. + public let mapMatchingResult: MapMatchingResult + /// Actual speed limit. + public let speedLimit: SpeedLimit + /// Detected actual user speed. + public let currentSpeed: Measurement + /// Current road name, if available. + public let roadName: RoadName? + + /// The best possible location update, snapped to the route or map matched to the road if possible + public var enhancedLocation: CLLocation { + mapMatchingResult.enhancedLocation + } +} + +// MARK: - FallbackToTilesState + +/// Tiles fallback update event details. +public struct FallbackToTilesState: Equatable, Sendable { + /// Flags if the Navigator is currently using latest known tiles version. + public let usingLatestTiles: Bool +} + +// MARK: - SpokenInstructionState + +/// Voice instructions update event details. +public struct SpokenInstructionState: Equatable, Sendable { + /// Actual ``SpokenInstruction`` to be pronounced. + public let spokenInstruction: SpokenInstruction +} + +// MARK: - VisualInstructionState + +/// Visual instructions update event details. +public struct VisualInstructionState: Equatable, Sendable { + /// Actual visual instruction to be displayed. + public let visualInstruction: VisualInstructionBanner +} + +// MARK: - WaypointArrivalStatus + +/// The base for all ``WaypointArrivalStatus`` events. +public protocol WaypointArrivalEvent: NavigationEvent {} + +/// Waypoint arrival update event details. +public struct WaypointArrivalStatus: Equatable, Sendable { + public static func == (lhs: WaypointArrivalStatus, rhs: WaypointArrivalStatus) -> Bool { + lhs.event.compare(to: rhs.event) + } + + /// Actual event details. + /// + /// See ``WaypointArrivalEvent`` implementations for possible event types. + public let event: any WaypointArrivalEvent + + public enum Events { + /// User has arrived to the final destination. + public struct ToFinalDestination: WaypointArrivalEvent, @unchecked Sendable { + /// Final destination waypoint. + public let destination: Waypoint + } + + /// User has arrived to the intermediate waypoint. + public struct ToWaypoint: WaypointArrivalEvent, @unchecked Sendable { + /// The waypoint user has arrived to. + public let waypoint: Waypoint + /// Waypoint's leg index. + public let legIndex: Int + } + + /// Next leg navigation has started. + public struct NextLegStarted: WaypointArrivalEvent, @unchecked Sendable { + /// New actual leg index in the route. + public let newLegIndex: Int + } + } +} + +// MARK: - ReroutingStatus + +/// The base for all ``ReroutingStatus`` events. +public protocol ReroutingEvent: NavigationEvent {} + +/// Rerouting update event details. +public struct ReroutingStatus: Equatable, Sendable { + public static func == (lhs: ReroutingStatus, rhs: ReroutingStatus) -> Bool { + lhs.event.compare(to: rhs.event) + } + + /// Actual event details. + /// + /// See ``ReroutingEvent`` implementations for possible event types. + public let event: any ReroutingEvent + + public enum Events { + /// Reroute event was triggered and SDK is currently fetching a new route. + public struct FetchingRoute: ReroutingEvent, Sendable {} + /// The reroute process was manually interrupted. + public struct Interrupted: ReroutingEvent, Sendable {} + /// The reroute process has failed with an error. + public struct Failed: ReroutingEvent, Sendable { + /// The underlying error. + public let error: DirectionsError + } + + /// The reroute process has successfully fetched a route and completed the process. + public struct Fetched: ReroutingEvent, Sendable {} + } +} + +// MARK: - AlternativesStatus + +/// The base for all ``AlternativesStatus`` events. +public protocol AlternativesEvent: NavigationEvent {} + +/// Continuous alternatives update event details. +public struct AlternativesStatus: Equatable, Sendable { + public static func == (lhs: AlternativesStatus, rhs: AlternativesStatus) -> Bool { + lhs.event.compare(to: rhs.event) + } + + /// Actual event details. + /// + /// See ``AlternativesEvent`` implementations for possible event types. + public let event: any AlternativesEvent + + public enum Events { + /// The list of actual continuous alternatives was updated. + public struct Updated: AlternativesEvent, Sendable { + /// Currently actual list of alternative routes. + public let actualAlternativeRoutes: [AlternativeRoute] + } + + /// The navigator switched to the alternative route. The previous main route is an alternative now. + public struct SwitchedToAlternative: AlternativesEvent, Sendable { + /// The current navigation routes after switching to the alternative route. + public let navigationRoutes: NavigationRoutes + } + } +} + +// MARK: - FasterRoutesStatus + +/// The base for all ``FasterRoutesStatus`` events. +public protocol FasterRoutesEvent: NavigationEvent {} + +/// Faster route update event details. +public struct FasterRoutesStatus: Equatable, Sendable { + public static func == (lhs: FasterRoutesStatus, rhs: FasterRoutesStatus) -> Bool { + lhs.event.compare(to: rhs.event) + } + + /// Actual event details. + /// + /// See ``FasterRoutesEvent`` implementations for possible event types. + public let event: any FasterRoutesEvent + + public enum Events { + /// The SDK has detected a faster route possibility. + public struct Detected: FasterRoutesEvent, Sendable {} + /// The SDK has applied the faster route. + public struct Applied: FasterRoutesEvent, Sendable {} + } +} + +// MARK: - RefreshingStatus + +/// The base for all ``RefreshingStatus`` events. +public protocol RefreshingEvent: NavigationEvent {} + +/// Route refreshing update event details. +public struct RefreshingStatus: Equatable, Sendable { + public static func == (lhs: RefreshingStatus, rhs: RefreshingStatus) -> Bool { + lhs.event.compare(to: rhs.event) + } + + /// Actual event details. + /// + /// See ``RefreshingEvent`` implementations for possible event types. + public let event: any RefreshingEvent + + public enum Events { + /// The route refreshing process has begun. + public struct Refreshing: RefreshingEvent, Sendable {} + /// The route has been refreshed. + public struct Refreshed: RefreshingEvent, Sendable {} + /// Indicates that current route's refreshing is no longer available. + /// + /// It is strongly recommended to request a new route. Refreshing TTL has expired and the route will no longer + /// recieve refreshing updates, which may lead to suboptimal navigation experience. + public struct Invalidated: RefreshingEvent, Sendable { + /// The routes for which refreshing is no longer available. + public let navigationRoutes: NavigationRoutes + } + } +} + +// MARK: - EHorizonStatus + +/// The base for all ``EHorizonStatus`` events. +public protocol EHorizonEvent: NavigationEvent {} + +/// Electronic horizon update event details. +public struct EHorizonStatus: Equatable, Sendable { + public static func == (lhs: EHorizonStatus, rhs: EHorizonStatus) -> Bool { + lhs.event.compare(to: rhs.event) + } + + /// Actual event details. + /// + /// See ``EHorizonEvent`` implementations for possible event types. + public let event: any EHorizonEvent + + public enum Events { + /// EH position withing the road graph has changed. + public struct PositionUpdated: Sendable, EHorizonEvent { + /// New EH position. + public let position: RoadGraph.Position + /// New starting edge of the graph + public let startingEdge: RoadGraph.Edge + /// Flags if MPP was updated. + public let updatesMostProbablePath: Bool + /// Distances for upcoming road objects. + public let distances: [DistancedRoadObject] + } + + /// EH position has entered a road object. + public struct RoadObjectEntered: Sendable, EHorizonEvent { + /// Related road object ID. + public let roadObjectId: RoadObject.Identifier + /// Flags if entrance was from object's beginning. + public let enteredFromStart: Bool + } + + /// EH position has left a road object + public struct RoadObjectExited: Sendable, EHorizonEvent { + /// Related road object ID. + public let roadObjectId: RoadObject.Identifier + /// Flags if object was left through it's ending. + public let exitedFromEnd: Bool + } + + /// EH position has passed point or gantry objects + public struct RoadObjectPassed: Sendable, EHorizonEvent { + /// Related road object ID. + public let roadObjectId: RoadObject.Identifier + } + } +} + +// MARK: - NavigatorError + +/// The base for all ``NavigatorErrors``. +public protocol NavigatorError: Error {} + +public enum NavigatorErrors { + /// The SDK has failed to set a route to the Navigator. + public struct FailedToSetRoute: NavigatorError { + /// Underlying error description. + public let underlyingError: Error? + } + + /// Switching to the alternative route has failed. + public struct FailedToSelectAlternativeRoute: NavigatorError {} + /// Updating the list of alternative routes has failed. + public struct FailedToUpdateAlternativeRoutes: NavigatorError { + /// Localized description. + public let localizedDescription: String + } + + /// Switching route legs has failed. + public struct FailedToSelectRouteLeg: NavigatorError {} + /// Failed to switch the navigator state to `idle`. + public struct FailedToSetToIdle: NavigatorError {} + /// Failed to pause the free drive session. + public struct FailedToPause: NavigatorError {} + /// Unexpectedly received NN status when in `idle` state. + public struct UnexpectedNavigationStatus: NavigatorError {} + /// Rerouting process was not completed successfully. + public struct InterruptedReroute: NavigatorError { + /// Underlying error description. + public let underlyingError: Error? + } +} + +// MARK: - RoadMatching + +/// Description of the road graph network and related road objects. +public struct RoadMatching: Sendable { + /// Provides access to the road tree graph. + public let roadGraph: RoadGraph + /// Provides access to metadata about road objects. + public let roadObjectStore: RoadObjectStore + /// Provides methods for road object matching. + public let roadObjectMatcher: RoadObjectMatcher +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RoadInfo.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RoadInfo.swift new file mode 100644 index 000000000..d3f07b9b3 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RoadInfo.swift @@ -0,0 +1,40 @@ +// +// RoadInfo.swift +// +// +// Created by Maksim Chizhavko on 1/17/24. +// + +import Foundation +import MapboxDirections + +public struct RoadInfo: Equatable, Sendable { + /// the country code (ISO-2 format) of the road + public let countryCodeIso2: String? + + /// right-hand or left-hand traffic type + public let drivingSide: DrivingSide + + /// true if current road is one-way. + public let isOneWay: Bool + + /// the number of lanes + public let laneCount: Int? + + /// The edge’s general road classes. + public let roadClasses: RoadClasses + + public init( + countryCodeIso2: String?, + drivingSide: DrivingSide, + isOneWay: Bool, + laneCount: Int?, + roadClasses: RoadClasses + ) { + self.countryCodeIso2 = countryCodeIso2 + self.drivingSide = drivingSide + self.isOneWay = isOneWay + self.laneCount = laneCount + self.roadClasses = roadClasses + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteLegProgress.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteLegProgress.swift new file mode 100644 index 000000000..b4723c268 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteLegProgress.swift @@ -0,0 +1,231 @@ +import CoreLocation +import Foundation +import MapboxDirections +import MapboxNavigationNative + +/// ``RouteLegProgress`` stores the user’s progress along a route leg. +public struct RouteLegProgress: Equatable, Sendable { + // MARK: Details About the Leg + + mutating func update(using status: NavigationStatus) { + guard let activeGuidanceInfo = status.activeGuidanceInfo else { + return + } + + let statusStepIndex = Int(status.stepIndex) + guard leg.steps.indices ~= statusStepIndex else { + Log.error("Incorrect step index update: \(statusStepIndex)", category: .navigation) + return + } + + if stepIndex == statusStepIndex { + currentStepProgress.update(using: status) + } else { + var stepProgress = RouteStepProgress(step: leg.steps[statusStepIndex]) + stepProgress.update(using: status) + currentStepProgress = stepProgress + } + + stepIndex = statusStepIndex + shapeIndex = Int(status.shapeIndex) + + currentSpeedLimit = nil + if let speed = status.speedLimit.speed?.doubleValue { + switch status.speedLimit.localeUnit { + case .milesPerHour: + currentSpeedLimit = Measurement(value: speed, unit: .milesPerHour) + case .kilometresPerHour: + currentSpeedLimit = Measurement(value: speed, unit: .kilometersPerHour) + @unknown default: + assertionFailure("Unknown native speed limit unit.") + } + } + + distanceTraveled = activeGuidanceInfo.legProgress.distanceTraveled + durationRemaining = activeGuidanceInfo.legProgress.remainingDuration + distanceRemaining = activeGuidanceInfo.legProgress.remainingDistance + fractionTraveled = activeGuidanceInfo.legProgress.fractionTraveled + + if remainingSteps.count <= 2, status.routeState == .complete { + userHasArrivedAtWaypoint = true + } + } + + /// Returns the current ``RouteLeg``. + public private(set) var leg: RouteLeg + + /// Total distance traveled in meters along current leg. + public private(set) var distanceTraveled: CLLocationDistance = 0 + + /// Duration remaining in seconds on current leg. + public private(set) var durationRemaining: TimeInterval = 0 + + /// Distance remaining on the current leg. + public private(set) var distanceRemaining: CLLocationDistance = 0 + + /// Number between 0 and 1 representing how far along the current leg the user has traveled. + public private(set) var fractionTraveled: Double = 0 + + public var userHasArrivedAtWaypoint = false + + // MARK: Details About the Leg’s Steps + + /// Index representing the current step. + public private(set) var stepIndex: Int = 0 + + /// The remaining steps for user to complete. + public var remainingSteps: [RouteStep] { + return Array(leg.steps.suffix(from: stepIndex + 1)) + } + + /// Returns the ``RouteStep`` before a given step. Returns `nil` if there is no step prior. + public func stepBefore(_ step: RouteStep) -> RouteStep? { + guard let index = leg.steps.firstIndex(of: step) else { + return nil + } + if index > 0 { + return leg.steps[index - 1] + } + return nil + } + + /// Returns the ``RouteStep`` after a given step. Returns `nil` if there is not a step after. + public func stepAfter(_ step: RouteStep) -> RouteStep? { + guard let index = leg.steps.firstIndex(of: step) else { + return nil + } + if index + 1 < leg.steps.endIndex { + return leg.steps[index + 1] + } + return nil + } + + /// Returns the ``RouteStep`` before the current step. + /// + /// If there is no ``priorStep``, `nil` is returned. + public var priorStep: RouteStep? { + guard stepIndex - 1 >= 0 else { + return nil + } + return leg.steps[stepIndex - 1] + } + + /// Returns the current ``RouteStep`` for the leg the user is on. + public var currentStep: RouteStep { + return leg.steps[stepIndex] + } + + /// Returns the ``RouteStep`` after the current step. + /// + /// If there is no ``upcomingStep``, `nil` is returned. + public var upcomingStep: RouteStep? { + guard stepIndex + 1 < leg.steps.endIndex else { + return nil + } + return leg.steps[stepIndex + 1] + } + + /// Returns step 2 steps ahead. + /// + /// If there is no ``followOnStep``, `nil` is returned. + public var followOnStep: RouteStep? { + guard stepIndex + 2 < leg.steps.endIndex else { + return nil + } + return leg.steps[stepIndex + 2] + } + + /// Return bool whether step provided is the current ``RouteStep`` the user is on. + public func isCurrentStep(_ step: RouteStep) -> Bool { + return step == currentStep + } + + /// Returns the progress along the current ``RouteStep``. + public internal(set) var currentStepProgress: RouteStepProgress + + /// Returns the SpeedLimit for the current position along the route. Returns SpeedLimit.invalid if the speed limit + /// is unknown or missing. + /// + /// The maximum speed may be an advisory speed limit for segments where legal limits are not posted, such as highway + /// entrance and exit ramps. If the speed limit along a particular segment is unknown, it is set to `nil`. If the + /// speed is unregulated along the segment, such as on the German _Autobahn_ system, it is represented by a + /// measurement whose value is `Double.infinity`. + /// + /// Speed limit data is available in [a number of countries and territories + /// worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/). + public private(set) var currentSpeedLimit: Measurement? = nil + + /// Index relative to leg shape, representing the point the user is currently located at. + public private(set) var shapeIndex: Int = 0 + + /// Intializes a new ``RouteLegProgress``. + /// - Parameter leg: Leg on a ``NavigationRoute``. + public init(leg: RouteLeg) { + precondition( + leg.steps.indices.contains(stepIndex), + "It's not possible to set the stepIndex: \(stepIndex) when it's higher than steps count \(leg.steps.count) or not included." + ) + + self.leg = leg + + self.currentStepProgress = RouteStepProgress(step: leg.steps[stepIndex]) + } + + func refreshingLeg(with leg: RouteLeg) -> RouteLegProgress { + var refreshedProgress = self + + refreshedProgress.leg = leg + refreshedProgress.currentStepProgress = refreshedProgress.currentStepProgress + .refreshingStep(with: leg.steps[stepIndex]) + + return refreshedProgress + } + + typealias StepIndexDistance = (index: Int, distance: CLLocationDistance) + + func closestStep(to coordinate: CLLocationCoordinate2D) -> StepIndexDistance? { + var currentClosest: StepIndexDistance? + let remainingSteps = leg.steps.suffix(from: stepIndex) + + for (currentStepIndex, step) in remainingSteps.enumerated() { + guard let shape = step.shape else { continue } + guard let closestCoordOnStep = shape.closestCoordinate(to: coordinate) else { continue } + let closesCoordOnStepDistance = closestCoordOnStep.coordinate.distance(to: coordinate) + let foundIndex = currentStepIndex + stepIndex + + // First time around, currentClosest will be `nil`. + guard let currentClosestDistance = currentClosest?.distance else { + currentClosest = (index: foundIndex, distance: closesCoordOnStepDistance) + continue + } + + if closesCoordOnStepDistance < currentClosestDistance { + currentClosest = (index: foundIndex, distance: closesCoordOnStepDistance) + } + } + + return currentClosest + } + + /// The waypoints remaining on the current leg, not including the leg’s destination. + func remainingWaypoints(among waypoints: [MapboxDirections.Waypoint]) -> [MapboxDirections.Waypoint] { + guard waypoints.count > 1 else { + // The leg has only a source and no via points. Save ourselves a call to RouteLeg.coordinates, which can be + // expensive. + return [] + } + let legPolyline = leg.shape + guard let userCoordinateIndex = legPolyline.indexedCoordinateFromStart(distance: distanceTraveled)?.index else { + // The leg is empty, so none of the waypoints are meaningful. + return [] + } + var slice = legPolyline + var accumulatedCoordinates = 0 + return Array(waypoints.drop { waypoint -> Bool in + let newSlice = slice.sliced(from: waypoint.coordinate)! + accumulatedCoordinates += slice.coordinates.count - newSlice.coordinates.count + slice = newSlice + return accumulatedCoordinates <= userCoordinateIndex + }) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteProgress.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteProgress.swift new file mode 100644 index 000000000..bd213370d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteProgress.swift @@ -0,0 +1,419 @@ +import CoreLocation +import Foundation +import MapboxDirections +import class MapboxNavigationNative.NavigationStatus +import class MapboxNavigationNative.UpcomingRouteAlert +import Turf + +/// ``RouteProgress`` stores the user’s progress along a route. +public struct RouteProgress: Equatable, Sendable { + private static let reroutingAccuracy: CLLocationAccuracy = 90 + + /// Initializes a new ``RouteProgress``. + /// - Parameters: + /// - navigationRoutes: The selection of routes to follow. + /// - waypoints: The waypoints of the routes. + /// - congestionConfiguration: The congestion configuration to use to display the routes. + public init( + navigationRoutes: NavigationRoutes, + waypoints: [Waypoint], + congestionConfiguration: CongestionRangesConfiguration = .default + ) { + self.navigationRoutes = navigationRoutes + self.waypoints = waypoints + + self.currentLegProgress = RouteLegProgress(leg: navigationRoutes.mainRoute.route.legs[legIndex]) + + self.routeAlerts = routeAlerts(from: navigationRoutes.mainRoute) + calculateLegsCongestion(configuration: congestionConfiguration) + } + + /// Current `RouteOptions`, optimized for rerouting. + /// + /// This method is useful for implementing custom rerouting. Resulting `RouteOptions` skip passed waypoints and + /// include current user heading if possible. + /// - Parameters: + /// - location: Current user location. Treated as route origin for rerouting. + /// - routeOptions: The initial `RouteOptions`. + /// - Returns: Modified `RouteOptions`. + func reroutingOptions(from location: CLLocation, routeOptions: RouteOptions) -> RouteOptions { + let oldOptions = routeOptions + var user = Waypoint(coordinate: location.coordinate) + + // A pedestrian can turn on a dime; there's no problem with a route that starts out by turning the pedestrian + // around. + let transportType = currentLegProgress.currentStep.transportType + if transportType != .walking, location.course >= 0 { + user.heading = location.course + user.headingAccuracy = RouteProgress.reroutingAccuracy + } + let newWaypoints = [user] + remainingWaypointsForCalculatingRoute() + let newOptions: RouteOptions + do { + newOptions = try oldOptions.copy() + } catch { + newOptions = oldOptions + } + newOptions.waypoints = newWaypoints + + return newOptions + } + + // MARK: Route Statistics + + mutating func update(using status: NavigationStatus) { + guard let activeGuidanceInfo = status.activeGuidanceInfo else { + return + } + + if legIndex == Int(status.legIndex) { + currentLegProgress.update(using: status) + } else { + let legIndex = Int(status.legIndex) + guard route.legs.indices.contains(legIndex) else { + Log.info("Ignoring incorrect status update with leg index \(legIndex)", category: .navigation) + return + } + var leg = RouteLegProgress(leg: route.legs[legIndex]) + leg.update(using: status) + currentLegProgress = leg + } + + upcomingRouteAlerts = status.upcomingRouteAlertUpdates.compactMap { routeAlert in + routeAlerts[routeAlert.id].map { + RouteAlert($0, distanceToStart: routeAlert.distanceToStart) + } + } + shapeIndex = Int(status.geometryIndex) + legIndex = Int(status.legIndex) + + updateDistanceToIntersection() + + distanceTraveled = activeGuidanceInfo.routeProgress.distanceTraveled + durationRemaining = activeGuidanceInfo.routeProgress.remainingDuration + fractionTraveled = activeGuidanceInfo.routeProgress.fractionTraveled + distanceRemaining = activeGuidanceInfo.routeProgress.remainingDistance + } + + mutating func updateAlternativeRoutes(using navigationRoutes: NavigationRoutes) { + guard self.navigationRoutes.mainRoute == navigationRoutes.mainRoute, + self.navigationRoutes.alternativeRoutes.map(\.routeId) != navigationRoutes.alternativeRoutes + .map(\.routeId) + else { + return + } + self.navigationRoutes = navigationRoutes + } + + func refreshingRoute( + with refreshedRoutes: NavigationRoutes, + legIndex: Int, + legShapeIndex: Int, + congestionConfiguration: CongestionRangesConfiguration + ) -> RouteProgress { + var refreshedRouteProgress = self + + refreshedRouteProgress.routeAlerts = routeAlerts(from: refreshedRoutes.mainRoute) + + refreshedRouteProgress.navigationRoutes = refreshedRoutes + + refreshedRouteProgress.currentLegProgress = refreshedRouteProgress.currentLegProgress + .refreshingLeg(with: refreshedRouteProgress.route.legs[legIndex]) + refreshedRouteProgress.calculateLegsCongestion(configuration: congestionConfiguration) + + return refreshedRouteProgress + } + + public let waypoints: [Waypoint] + + /// Total distance traveled by user along all legs. + public private(set) var distanceTraveled: CLLocationDistance = 0 + + /// Total seconds remaining on all legs. + public private(set) var durationRemaining: TimeInterval = 0 + + /// Number between 0 and 1 representing how far along the `Route` the user has traveled. + public private(set) var fractionTraveled: Double = 0 + + /// Total distance remaining in meters along route. + public private(set) var distanceRemaining: CLLocationDistance = 0 + + /// The waypoints remaining on the current route. + /// + /// This property does not include waypoints whose `Waypoint.separatesLegs` property is set to `false`. + public var remainingWaypoints: [Waypoint] { + return route.legs.suffix(from: legIndex).compactMap(\.destination) + } + + func waypoints(fromLegAt legIndex: Int) -> ([Waypoint], [Waypoint]) { + // The first and last waypoints always separate legs. Make exceptions for these waypoints instead of modifying + // them by side effect. + let legSeparators = waypoints.filterKeepingFirstAndLast { $0.separatesLegs } + let viaPointsByLeg = waypoints.splitExceptAtStartAndEnd(omittingEmptySubsequences: false) { $0.separatesLegs } + .dropFirst() // No leg precedes first separator. + + let reconstitutedWaypoints = zip(legSeparators, viaPointsByLeg).dropFirst(legIndex).map { [$0.0] + $0.1 } + let legWaypoints = reconstitutedWaypoints.first ?? [] + let subsequentWaypoints = reconstitutedWaypoints.dropFirst() + return (legWaypoints, subsequentWaypoints.flatMap { $0 }) + } + + /// The waypoints remaining on the current route, including any waypoints that do not separate legs. + func remainingWaypointsForCalculatingRoute() -> [Waypoint] { + let (currentLegViaPoints, remainingWaypoints) = waypoints(fromLegAt: legIndex) + let currentLegRemainingViaPoints = currentLegProgress.remainingWaypoints(among: currentLegViaPoints) + return currentLegRemainingViaPoints + remainingWaypoints + } + + /// Upcoming ``RouteAlert``s as reported by the navigation engine. + /// + /// The contents of the array depend on user's current progress along the route and are modified on each location + /// update. This array contains only the alerts that the user has not passed. Some events may have non-zero length + /// and are also included while the user is traversing it. You can use this property to get information about + /// incoming points of interest. + public private(set) var upcomingRouteAlerts: [RouteAlert] = [] + private(set) var routeAlerts: [String: UpcomingRouteAlert] = [:] + + private func routeAlerts(from navigationRoute: NavigationRoute) -> [String: UpcomingRouteAlert] { + return navigationRoute.nativeRoute.getRouteInfo().alerts.reduce(into: [:]) { partialResult, alert in + partialResult[alert.roadObject.id] = alert + } + } + + /// Returns an array of `CLLocationCoordinate2D` of the coordinates along the current step and any adjacent steps. + /// + /// - Important: The adjacent steps may be part of legs other than the current leg. + public var nearbyShape: LineString { + let priorCoordinates = priorStep?.shape?.coordinates.dropLast() ?? [] + let currentShape = currentLegProgress.currentStep.shape + let upcomingCoordinates = upcomingStep?.shape?.coordinates.dropFirst() ?? [] + if let currentShape, priorCoordinates.isEmpty, upcomingCoordinates.isEmpty { + return currentShape + } + return LineString(priorCoordinates + (currentShape?.coordinates ?? []) + upcomingCoordinates) + } + + // MARK: Updating the RouteProgress + + /// Returns the current ``NavigationRoutes``. + public private(set) var navigationRoutes: NavigationRoutes + + /// Returns the current main `Route`. + public var route: Route { + navigationRoutes.mainRoute.route + } + + public var routeId: RouteId { + navigationRoutes.mainRoute.routeId + } + + /// Index relative to route shape, representing the point the user is currently located at. + public private(set) var shapeIndex: Int = 0 + + /// Update the distance to intersection according to new location specified. + private mutating func updateDistanceToIntersection() { + guard var intersections = currentLegProgress.currentStepProgress.step.intersections else { return } + + // The intersections array does not include the upcoming maneuver intersection. + if let upcomingIntersection = currentLegProgress.upcomingStep?.intersections?.first { + intersections += [upcomingIntersection] + } + currentLegProgress.currentStepProgress.update(intersectionsIncludingUpcomingManeuverIntersection: intersections) + + if let shape = currentLegProgress.currentStep.shape, + let upcomingIntersection = currentLegProgress.currentStepProgress.upcomingIntersection, + let coordinateOnStep = shape.coordinateFromStart( + distance: currentLegProgress.currentStepProgress.distanceTraveled + ) + { + currentLegProgress.currentStepProgress.userDistanceToUpcomingIntersection = shape.distance( + from: coordinateOnStep, + to: upcomingIntersection.location + ) + } + } + + // MARK: Leg Statistics + + /// Index representing current ``RouteLeg``. + public private(set) var legIndex: Int = 0 + + /// If waypoints are provided in the `Route`, this will contain which leg the user is on. + public var currentLeg: RouteLeg { + return route.legs[legIndex] + } + + /// Returns the remaining legs left on the current route + public var remainingLegs: [RouteLeg] { + return Array(route.legs.suffix(from: legIndex + 1)) + } + + /// Returns true if ``currentLeg`` is the last leg. + public var isFinalLeg: Bool { + guard let lastLeg = route.legs.last else { return false } + return currentLeg == lastLeg + } + + /// Returns the progress along the current ``RouteLeg``. + public var currentLegProgress: RouteLegProgress + + /// The previous leg. + public var priorLeg: RouteLeg? { + return legIndex > 0 ? route.legs[legIndex - 1] : nil + } + + /// The leg following the current leg along this route. + /// + /// If this leg is the last leg of the route, this property is set to nil. + public var upcomingLeg: RouteLeg? { + return legIndex + 1 < route.legs.endIndex ? route.legs[legIndex + 1] : nil + } + + // MARK: Step Statistics + + /// Returns the remaining steps left on the current route + public var remainingSteps: [RouteStep] { + return currentLegProgress.remainingSteps + remainingLegs.flatMap(\.steps) + } + + /// The step prior to the current step along this route. + /// + /// The prior step may be part of a different RouteLeg than the current step. If the current step is the first step + /// along the route, this property is set to nil. + public var priorStep: RouteStep? { + return currentLegProgress.priorStep ?? priorLeg?.steps.last + } + + /// The step following the current step along this route. + /// + /// The upcoming step may be part of a different ``RouteLeg`` than the current step. If it is the last step along + /// the route, this property is set to nil. + public var upcomingStep: RouteStep? { + return currentLegProgress.upcomingStep ?? upcomingLeg?.steps.first + } + + // MARK: Leg Attributes + + /// The struc containing a ``CongestionLevel`` and a corresponding `TimeInterval` representing the expected travel + /// time for this segment. + public struct TimedCongestionLevel: Equatable, Sendable { + public var level: CongestionLevel + public var timeInterval: TimeInterval + } + + /// If the route contains both `RouteLeg.segmentCongestionLevels` and `RouteLeg.expectedSegmentTravelTimes`, this + /// property is set + /// to a deeply nested array of ``RouteProgress/TimedCongestionLevel`` per segment per step per leg. + public private(set) var congestionTravelTimesSegmentsByStep: [[[TimedCongestionLevel]]] = [] + + /// An dictionary containing a `TimeInterval` total per ``CongestionLevel``. Only ``CongestionLevel`` found on that + /// step will present. Broken up by leg and then step. + public private(set) var congestionTimesPerStep: [[[CongestionLevel: TimeInterval]]] = [[[:]]] + + public var averageCongestionLevelRemainingOnLeg: CongestionLevel? { + guard let coordinates = currentLegProgress.currentStepProgress.step.shape?.coordinates else { + return .unknown + } + + let coordinatesLeftOnStepCount = + Int(floor(Double(coordinates.count) * currentLegProgress.currentStepProgress.fractionTraveled)) + + guard coordinatesLeftOnStepCount >= 0 else { return .unknown } + + guard legIndex < congestionTravelTimesSegmentsByStep.count, + currentLegProgress.stepIndex < congestionTravelTimesSegmentsByStep[legIndex].count + else { return .unknown } + + let congestionTimesForStep = congestionTravelTimesSegmentsByStep[legIndex][currentLegProgress.stepIndex] + guard coordinatesLeftOnStepCount <= congestionTimesForStep.count else { return .unknown } + + let remainingCongestionTimesForStep = congestionTimesForStep.suffix(from: coordinatesLeftOnStepCount) + let remainingCongestionTimesForRoute = congestionTimesPerStep[legIndex] + .suffix(from: currentLegProgress.stepIndex + 1) + + var remainingStepCongestionTotals: [CongestionLevel: TimeInterval] = [:] + for stepValues in remainingCongestionTimesForRoute { + for (key, value) in stepValues { + remainingStepCongestionTotals[key] = (remainingStepCongestionTotals[key] ?? 0) + value + } + } + + for remainingCongestionTimeForStep in remainingCongestionTimesForStep { + let segmentCongestion = remainingCongestionTimeForStep.level + let segmentTime = remainingCongestionTimeForStep.timeInterval + remainingStepCongestionTotals[segmentCongestion] = (remainingStepCongestionTotals[segmentCongestion] ?? 0) + + segmentTime + } + + if durationRemaining < 60 { + return .unknown + } else { + if let max = remainingStepCongestionTotals.max(by: { a, b in a.value < b.value }) { + return max.key + } else { + return .unknown + } + } + } + + mutating func calculateLegsCongestion(configuration: CongestionRangesConfiguration) { + congestionTimesPerStep.removeAll() + congestionTravelTimesSegmentsByStep.removeAll() + + for (legIndex, leg) in route.legs.enumerated() { + var maneuverCoordinateIndex = 0 + + congestionTimesPerStep.append([]) + + /// An index into the route’s coordinates and congestionTravelTimesSegmentsByStep that corresponds to a + /// step’s maneuver location. + var congestionTravelTimesSegmentsByLeg: [[TimedCongestionLevel]] = [] + + if let segmentCongestionLevels = leg.resolveCongestionLevels(using: configuration), + let expectedSegmentTravelTimes = leg.expectedSegmentTravelTimes + { + for step in leg.steps { + guard let coordinates = step.shape?.coordinates else { continue } + let stepCoordinateCount = step.maneuverType == .arrive ? Int(coordinates.count) : coordinates + .dropLast().count + let nextManeuverCoordinateIndex = maneuverCoordinateIndex + stepCoordinateCount - 1 + + guard nextManeuverCoordinateIndex < segmentCongestionLevels.count else { continue } + guard nextManeuverCoordinateIndex < expectedSegmentTravelTimes.count else { continue } + + let stepSegmentCongestionLevels = + Array(segmentCongestionLevels[maneuverCoordinateIndex.. RouteStepProgress { + var refreshedProgress = self + + refreshedProgress.step = step + + return refreshedProgress + } + + // MARK: Step Stats + + mutating func update(using status: NavigationStatus) { + guard let activeGuidanceInfo = status.activeGuidanceInfo else { + return + } + + distanceTraveled = activeGuidanceInfo.stepProgress.distanceTraveled + distanceRemaining = activeGuidanceInfo.stepProgress.remainingDistance + fractionTraveled = activeGuidanceInfo.stepProgress.fractionTraveled + durationRemaining = activeGuidanceInfo.stepProgress.remainingDuration + + intersectionIndex = Int(status.intersectionIndex) + visualInstructionIndex = status.bannerInstruction.map { Int($0.index) } ?? visualInstructionIndex + // TODO: ensure NN fills these only when it is really needed (mind reroutes/alternatives switch/etc) + spokenInstructionIndex = status.voiceInstruction.map { Int($0.index) } + currentSpokenInstruction = status.voiceInstruction.map(SpokenInstruction.init) + } + + /// Returns the current ``RouteStep``. + public private(set) var step: RouteStep + + /// Returns distance user has traveled along current step. + public private(set) var distanceTraveled: CLLocationDistance = 0 + + /// Total distance in meters remaining on current step. + public private(set) var distanceRemaining: CLLocationDistance = 0 + + /// Number between 0 and 1 representing fraction of current step traveled. + public private(set) var fractionTraveled: Double = 0 + + /// Number of seconds remaining on current step. + public private(set) var durationRemaining: TimeInterval = 0 + + /// Returns remaining step shape coordinates. + public func remainingStepCoordinates() -> [CLLocationCoordinate2D] { + guard let shape = step.shape else { + return [] + } + + guard let indexedStartCoordinate = shape.indexedCoordinateFromStart(distance: distanceTraveled) else { + return [] + } + + return Array(shape.coordinates.suffix(from: indexedStartCoordinate.index)) + } + + // MARK: Intersections + + /// All intersections on the current ``RouteStep`` and also the first intersection on the upcoming ``RouteStep``. + /// + /// The upcoming RouteStep first Intersection is added because it is omitted from the current step. + public var intersectionsIncludingUpcomingManeuverIntersection: [Intersection]? + + mutating func update(intersectionsIncludingUpcomingManeuverIntersection newValue: [Intersection]?) { + intersectionsIncludingUpcomingManeuverIntersection = newValue + } + + /// The next intersection the user will travel through. + /// The step must contain ``intersectionsIncludingUpcomingManeuverIntersection`` otherwise this property will be + /// `nil`. + public var upcomingIntersection: Intersection? { + guard let intersections = intersectionsIncludingUpcomingManeuverIntersection, intersections.count > 0, + intersections.startIndex.. { + return ["step.instructionsDisplayedAlongStep", "visualInstructionIndex"] + } + + public var keyPathsAffectingValueForRemainingSpokenInstructions: Set { + return ["step.instructionsDisplayedAlongStep", "spokenInstructionIndex"] + } +} + +extension SpokenInstruction { + init(_ nativeInstruction: VoiceInstruction) { + self.init( + distanceAlongStep: LocationDistance(nativeInstruction.remainingStepDistance), // is it the same distance? + text: nativeInstruction.announcement, + ssmlText: nativeInstruction.ssmlAnnouncement + ) + } +} + +extension VisualInstructionBanner { + init(_ nativeInstruction: BannerInstruction) { + let drivingSide: DrivingSide = if let nativeDrivingSide = nativeInstruction.primary.drivingSide, + let converted = DrivingSide(rawValue: nativeDrivingSide) + { + converted + } else { + .right + } + + self.init( + distanceAlongStep: LocationDistance(nativeInstruction.remainingStepDistance), + primary: .init(nativeInstruction.primary), + secondary: nativeInstruction.secondary.map(VisualInstruction.init), + tertiary: nativeInstruction.sub.map(VisualInstruction.init), + quaternary: nativeInstruction.view.map(VisualInstruction.init), + drivingSide: drivingSide + ) + } +} + +extension VisualInstruction { + init(_ nativeInstruction: BannerSection) { + let maneuverType = nativeInstruction.type.map(ManeuverType.init(rawValue:)) ?? nil + let maneuverDirection = nativeInstruction.modifier.map(ManeuverDirection.init(rawValue:)) ?? nil + let components = nativeInstruction.components?.map(VisualInstruction.Component.init) ?? [] + + self.init( + text: nativeInstruction.text, + maneuverType: maneuverType, + maneuverDirection: maneuverDirection, + components: components, + degrees: nativeInstruction.degrees?.doubleValue + ) + } +} + +extension VisualInstruction.Component { + init(_ nativeComponent: BannerComponent) { + let textRepresentation = TextRepresentation( + text: nativeComponent.text, + abbreviation: nativeComponent.abbr, + abbreviationPriority: nativeComponent.abbrPriority?.intValue + ) + // TODO: get rid of constants + switch nativeComponent.type { + case "delimeter": + self = .delimiter(text: textRepresentation) + return + case "text": + self = .text(text: textRepresentation) + return + case "image": + guard let nativeShield = nativeComponent.shield, + let baseURL = URL(string: nativeShield.baseUrl), + let imageBaseURL = nativeComponent.imageBaseUrl + else { + break + } + let shield = ShieldRepresentation( + baseURL: baseURL, + name: nativeShield.name, + textColor: nativeShield.textColor, + text: nativeShield.displayRef + ) + self = .image( + image: ImageRepresentation( + imageBaseURL: URL(string: imageBaseURL), + shield: shield + ), + alternativeText: textRepresentation + ) + return + case "guidance-view": + guard let imageURL = nativeComponent.imageURL else { + break + } + self = .guidanceView( + image: GuidanceViewImageRepresentation(imageURL: URL(string: imageURL)), + alternativeText: textRepresentation + ) + return + case "exit": + self = .exit(text: textRepresentation) + return + case "exit-number": + self = .exitCode(text: textRepresentation) + return + case "lane": + guard let directions = nativeComponent.directions, + let indications = LaneIndication(descriptions: directions) + else { + break + } + let activeDirection = nativeComponent.activeDirection + let preferredDirection = activeDirection.flatMap { ManeuverDirection(rawValue: $0) } + self = .lane( + indications: indications, + isUsable: nativeComponent.active?.boolValue ?? false, + preferredDirection: preferredDirection + ) + return + default: + break + } + self = .text(text: textRepresentation) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/SpeedLimit.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/SpeedLimit.swift new file mode 100644 index 000000000..989b3da7b --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/SpeedLimit.swift @@ -0,0 +1,7 @@ +import Foundation +import MapboxDirections + +public struct SpeedLimit: Equatable, @unchecked Sendable { + public let value: Measurement? + public let signStandard: SignStandard +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Tunnel.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Tunnel.swift new file mode 100644 index 000000000..6154b5bbc --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Tunnel.swift @@ -0,0 +1,13 @@ + +import Foundation +import MapboxNavigationNative + +/// ``Tunnel`` is used for naming incoming tunnels, together with route alerts. +public struct Tunnel: Equatable { + /// The name of the tunnel. + public let name: String? + + init(_ tunnelInfo: TunnelInfo) { + self.name = tunnelInfo.name + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheConfig.swift new file mode 100644 index 000000000..ae7119d09 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheConfig.swift @@ -0,0 +1,29 @@ +import MapboxDirections +import MapboxNavigationNative + +/// Specifies the content that a predictive cache fetches and how it fetches the content. +public struct PredictiveCacheConfig: Equatable, Sendable { + /// Predictive cache Navigation related config + public var predictiveCacheNavigationConfig: PredictiveCacheNavigationConfig = .init() + + /// Predictive cache Map related config + public var predictiveCacheMapsConfig: PredictiveCacheMapsConfig = .init() + + /// Predictive cache Search domain related config + public var predictiveCacheSearchConfig: PredictiveCacheSearchConfig? = nil + + /// Creates a new `PredictiveCacheConfig` instance. + /// - Parameters: + /// - predictiveCacheNavigationConfig: Navigation related config. + /// - predictiveCacheMapsConfig: Map related config. + /// - predictiveCacheSearchConfig: Search related config + public init( + predictiveCacheNavigationConfig: PredictiveCacheNavigationConfig = .init(), + predictiveCacheMapsConfig: PredictiveCacheMapsConfig = .init(), + predictiveCacheSearchConfig: PredictiveCacheSearchConfig? = nil + ) { + self.predictiveCacheNavigationConfig = predictiveCacheNavigationConfig + self.predictiveCacheMapsConfig = predictiveCacheMapsConfig + self.predictiveCacheSearchConfig = predictiveCacheSearchConfig + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheLocationConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheLocationConfig.swift new file mode 100644 index 000000000..e2d59ce83 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheLocationConfig.swift @@ -0,0 +1,39 @@ +import MapboxNavigationNative + +/// Specifies the content that a predictive cache fetches and how it fetches the content. +public struct PredictiveCacheLocationConfig: Equatable, Sendable { + /// How far around the user's location caching is going to be performed. + /// + /// Defaults to 2000 meters. + public var currentLocationRadius: CLLocationDistance = 2000 + + /// How far around the active route caching is going to be performed (if route is set). + /// + /// Defaults to 500 meters. + public var routeBufferRadius: CLLocationDistance = 500 + + /// How far around the destination location caching is going to be performed (if route is set). + /// + /// Defaults to 5000 meters. + public var destinationLocationRadius: CLLocationDistance = 5000 + + public init( + currentLocationRadius: CLLocationDistance = 2000, + routeBufferRadius: CLLocationDistance = 500, + destinationLocationRadius: CLLocationDistance = 5000 + ) { + self.currentLocationRadius = currentLocationRadius + self.routeBufferRadius = routeBufferRadius + self.destinationLocationRadius = destinationLocationRadius + } +} + +extension PredictiveLocationTrackerOptions { + convenience init(_ locationOptions: PredictiveCacheLocationConfig) { + self.init( + currentLocationRadius: UInt32(locationOptions.currentLocationRadius), + routeBufferRadius: UInt32(locationOptions.routeBufferRadius), + destinationLocationRadius: UInt32(locationOptions.destinationLocationRadius) + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheManager.swift new file mode 100644 index 000000000..54ea5903a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheManager.swift @@ -0,0 +1,111 @@ +import Foundation +import MapboxCommon +import MapboxMaps +import MapboxNavigationNative + +/// Proactively fetches tiles which may become necessary if the device loses its Internet connection at some point +/// during passive or active turn-by-turn navigation. +/// +/// Typically, you initialize an instance of this class and retain it as long as caching is required. Pass +/// ``MapboxNavigationProvider/predictiveCacheManager`` to your ``NavigationMapView`` instance to use predictive cache. +/// - Note: This object uses global tile store configuration from ``CoreConfig/predictiveCacheConfig``. +public class PredictiveCacheManager { + private let predictiveCacheOptions: PredictiveCacheConfig + private let tileStore: TileStore + + private weak var navigator: NavigationNativeNavigator? { + didSet { + _ = mapTilesetDescriptor.map { descriptor in + Task { @MainActor in + self.mapController = createMapController(descriptor) + } + } + } + } + + private var mapTilesetDescriptor: TilesetDescriptor? + private var navigationController: PredictiveCacheController? + private var mapController: PredictiveCacheController? + private var searchController: PredictiveCacheController? + + init( + predictiveCacheOptions: PredictiveCacheConfig, + tileStore: TileStore, + styleSourcePaths: [String] = [] + ) { + self.predictiveCacheOptions = predictiveCacheOptions + self.tileStore = tileStore + } + + @MainActor + public func updateMapControllers(mapView: MapView) { + let mapsOptions = predictiveCacheOptions.predictiveCacheMapsConfig + let tilesetDescriptor = mapView.tilesetDescriptor(zoomRange: mapsOptions.zoomRange) + mapTilesetDescriptor = tilesetDescriptor + mapController = createMapController(tilesetDescriptor) + } + + @MainActor + func updateNavigationController(with navigator: NavigationNativeNavigator?) { + self.navigator = navigator + navigationController = createNavigationController(for: navigator) + } + + @MainActor + func updateSearchController(with navigator: NavigationNativeNavigator?) { + self.navigator = navigator + searchController = createSearchController(for: navigator) + } + + @MainActor + private func createMapController(_ tilesetDescriptor: TilesetDescriptor?) -> PredictiveCacheController? { + guard let tilesetDescriptor else { return nil } + + let cacheMapsOptions = predictiveCacheOptions.predictiveCacheMapsConfig + let predictiveLocationTrackerOptions = PredictiveLocationTrackerOptions(cacheMapsOptions.locationConfig) + return navigator?.native.createPredictiveCacheController( + for: tileStore, + descriptors: [tilesetDescriptor], + locationTrackerOptions: predictiveLocationTrackerOptions + ) + } + + @MainActor + private func createNavigationController( + for navigator: NavigationNativeNavigator? + ) -> PredictiveCacheController? { + guard let navigator else { return nil } + + let locationOptions = predictiveCacheOptions.predictiveCacheNavigationConfig.locationConfig + let predictiveLocationTrackerOptions = PredictiveLocationTrackerOptions(locationOptions) + return navigator.native.createPredictiveCacheController(for: predictiveLocationTrackerOptions) + } + + @MainActor + /// Instantiate a controller for search functionality if the ``PredictiveCacheSearchConfig`` has the necessary + /// inputs. + /// Assign `tileStore.setOptionForKey("log-tile-loading", value: true)` and set MapboxCommon log level to info for + /// debug output + /// - Returns: A predictive cache controller configured for search functionality. + private func createSearchController(for navigator: NavigationNativeNavigator?) -> PredictiveCacheController? { + guard let navigator, + let predictiveCacheSearchConfig = predictiveCacheOptions.predictiveCacheSearchConfig + else { + return nil + } + + let locationOptions = predictiveCacheSearchConfig.locationConfig + let predictiveLocationTrackerOptions = PredictiveLocationTrackerOptions(locationOptions) + + return navigator.native.createPredictiveCacheController( + for: tileStore, + descriptors: [ + predictiveCacheSearchConfig.searchTilesetDescriptor, + ], + locationTrackerOptions: predictiveLocationTrackerOptions + ) + } +} + +extension TilesetDescriptor: @unchecked Sendable {} +extension PredictiveCacheManager: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheMapsConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheMapsConfig.swift new file mode 100644 index 000000000..5e94e641c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheMapsConfig.swift @@ -0,0 +1,31 @@ +import Foundation + +/// Specifies predictive cache Maps related config. +public struct PredictiveCacheMapsConfig: Equatable, Sendable { + /// Location configuration for visual map predictive caching. + public var locationConfig: PredictiveCacheLocationConfig = .init() + + /// Maxiumum amount of concurrent requests, which will be used for caching. + /// Defaults to 2 concurrent requests. + public var maximumConcurrentRequests: UInt32 = 2 + + /// Closed range zoom level for the tile package. + /// See `TilesetDescriptorOptionsForTilesets.minZoom` and `TilesetDescriptorOptionsForTilesets.maxZoom`. + /// Defaults to 0..16. + public var zoomRange: ClosedRange = 0...16 + + /// Creates a new ``PredictiveCacheMapsConfig`` instance. + /// - Parameters: + /// - locationConfig: Location configuration for visual map predictive caching. + /// - maximumConcurrentRequests: Maxiumum amount of concurrent requests, which will be used for caching. + /// - zoomRange: Closed range zoom level for the tile package. + public init( + locationConfig: PredictiveCacheLocationConfig = .init(), + maximumConcurrentRequests: UInt32 = 2, + zoomRange: ClosedRange = 0...16 + ) { + self.locationConfig = locationConfig + self.maximumConcurrentRequests = maximumConcurrentRequests + self.zoomRange = zoomRange + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheNavigationConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheNavigationConfig.swift new file mode 100644 index 000000000..902a37faa --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheNavigationConfig.swift @@ -0,0 +1,13 @@ +import Foundation + +/// Specifies predictive cache Navigation related config. +public struct PredictiveCacheNavigationConfig: Equatable, Sendable { + /// Location configuration for predictive caching. + public var locationConfig: PredictiveCacheLocationConfig = .init() + + /// Creates a new ``PredictiveCacheNavigationConfig`` instance. + /// - Parameter locationConfig: Location configuration for predictive caching. + public init(locationConfig: PredictiveCacheLocationConfig = .init()) { + self.locationConfig = locationConfig + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheSearchConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheSearchConfig.swift new file mode 100644 index 000000000..b9ee6008d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheSearchConfig.swift @@ -0,0 +1,25 @@ +import Foundation +import MapboxCommon + +/// Predictive cache search related options. +public struct PredictiveCacheSearchConfig: Equatable, Sendable { + /// Location configuration for visual map predictive caching. + public var locationConfig: PredictiveCacheLocationConfig = .init() + + /// TilesetDescriptor to use specifically for Search domain predictive cache. + /// Must be configured for Search tileset usage. + /// Required when used with `PredictiveCacheManager`. + public var searchTilesetDescriptor: TilesetDescriptor + + /// Create a new ``PredictiveCacheSearchConfig`` instance. + /// - Parameters: + /// - locationConfig: Location configuration for predictive caching. + /// - searchTilesetDescriptor: Required TilesetDescriptor for search domain predictive caching. + public init( + locationConfig: PredictiveCacheLocationConfig = .init(), + searchTilesetDescriptor: TilesetDescriptor + ) { + self.locationConfig = locationConfig + self.searchTilesetDescriptor = searchTilesetDescriptor + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/3DPuck.glb b/ios/Classes/Navigation/MapboxNavigationCore/Resources/3DPuck.glb new file mode 100644 index 0000000000000000000000000000000000000000..a7da8327173c474713a449c667e3a1a906b384f0 GIT binary patch literal 31480 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 IFA1#nKX*YLxc~qF literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/Contents.json new file mode 100644 index 000000000..f1f2e4e57 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "RailroadCrossing.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/RailroadCrossing.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/RailroadCrossing.pdf new file mode 100644 index 0000000000000000000000000000000000000000..aa12b023f964aa59146aa653262a32f036929e6f GIT binary patch literal 14532 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 literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/Contents.json new file mode 100644 index 000000000..433f9e4fa --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "YieldSign.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/YieldSign.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/YieldSign.pdf new file mode 100644 index 0000000000000000000000000000000000000000..36b6e28f8b6223ba7d5e777379a71bcaefbf2d9e GIT binary patch literal 10639 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 literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/Contents.json new file mode 100644 index 000000000..0813d4704 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ra_accident.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/ra_accident.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/ra_accident.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a78a61d965a8b1c7f1322697150857859c9283c8 GIT binary patch literal 18167 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%0DUxt4p5-HK2wRybH{I1~WF>9RPG9!pFJOBO7$ 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 AKL7v# literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/Contents.json new file mode 100644 index 000000000..09dcfecee --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ra_contruction.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/ra_contruction.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/ra_contruction.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3edfe5fcbd43bfb1760216b2d7b7517ba275026d GIT binary patch literal 15309 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 literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/Contents.json new file mode 100644 index 000000000..0cd578422 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ra_disabled_vehicle.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/ra_disabled_vehicle.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/ra_disabled_vehicle.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3928df0cd321a8c2d561dbb3f1a67119f62bfc78 GIT binary patch literal 16033 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 literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/Contents.json new file mode 100644 index 000000000..770f037dc --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ra_lane_restriction.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/ra_lane_restriction.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/ra_lane_restriction.pdf new file mode 100644 index 0000000000000000000000000000000000000000..807aa89e1dae6bdba8c96ee4c67e5b81db16fab0 GIT binary patch literal 17449 zcmeHv2{e^m`}ac>$E;+?aLhv-XC6dmnKMKsWfobs!_ukj=yRLohYv1e0TNxOsVNfIl z0)s>&{ayDUkVs8Ur23Zq0Uk*8ZJS;9c({cC41CmWLtR6tu!*nBZVx)B=N%GkT zx+>!+lAp~gUMY=Dz+iT1?3E0;YvYPvcIyXkB+ggVFRCSOl!_j{94>RTGfdiD=9tZ6 zk===`_lU!e#>(VtI~1>9vQSo5tk1G~>9D&(ko$GJh!bf{zc+6eCECl#2RCFq!4Z~T zChBnE`z94i$!qh%2*SelD-KUyt60V>uj@FVUHR~Z@y#IZj>tL#QcIQJp?wLT2uB>_ zv}y;IY8;>DxZoPMuKQ~D`M}EQb{CYg(^ivvyB2<4<87l9;Y!A5vZkYbtsjWQ_@6_b z?@>J+IB@T}lmFFPv^`;S#(T39)UN7XKv; zY&PlrBNm+Ows<0j;Xmr)KP?c&uBWH!nJQt18Z--#dZW32L{P0hzTo}|`_OwW-`kK5 z3vHh{j|l!cT+*3@d0FOd$NvViYInG%(cO4>D!2n>l_Lii?mGE zQoaS<5$BCxjN0s9)&JVBMCgUgrmb?{=Azg(m!vlxsP4Jc*&{JuVRwt|<7b1YGW*Nt zRtJ~#cU?}GP#+WYOXI>_d!)WFGjpLhw4%h*Z7oKbTTQ?@gSGtl>KiJ%ZXVs+^o%>F zJ#M?f4iSfzQUC4bWx^Lc(&eMfk5vrZDpP-?>S`l$iI~}*#~yPhUw*rTJrB_&M}A9& zLWRMNBAObM9HA{Gf@h$ffZ^hu(A^oSm5N z4={bi$+y=l;YfM=a5ws_wtv-IX=K66O`SKleRWhQ^*OqCcfzI_!!62s=yO*OZCK;| zsQhjg`6d@zd48T~@8jt--L(dXmO50`E9OqvHAa}7=kmR_C)hIUa@gK!(+rXGi^gh? zUX5<|3g&#cNmcr7<=U6Oj#4_6tbejx@HKb(8)8FTqY)cVBErNb!05k~*hD7U(z6)} zL+2)q#_*s2hHMs@#pQNGlRK8=f9&`^c0+^vKEHlRMRk}sJeYjF�@yq9d(S=H2kH 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 HW~2WF*8IeH literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/Contents.json new file mode 100644 index 000000000..81904c333 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ra_mass_transit.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/ra_mass_transit.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/ra_mass_transit.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8d8d1eab0bb9905bc7dc1e0df425ef222ad28d38 GIT binary patch literal 15442 zcmcJ02{@E{`~OK4lfA`~Wvn5MS)SR1?Aar6kg|)iZ>dlb(tvN)-L>XJ$O3^PcN{ulM@Df7iLrHGQ7%a<8BJ{(hes6`3_nFe@7_!#?m1$+D8*TGXgJzRK1n2$#wE_#;F!#^{2;YImc zgTfXW5Syo-FfS6DYS$d6JsY)H_C4mp#OmIk3qDl1CA~a*!*gG#HZe+Rceui?1@W_! z51OC8<|xhxNGEqVpZ&U@n13#f#20)-pMEs`{we=s;h#+vcl+LR&3bpGovFR7uJz31 zn#|HMmE`Ulh(cLp_mhmmBR|QDExXgg-5S021?WB)?R4KLnHkjCayjEmmf?k(2bC{I z?kxTD<$9mCK$TnDnQzN0MLIU$k@z-X)fBiqwLEjjP7=X6Yf;nXQfAgBTGPAL{^ez4 zri!W7{AhPVX@`va;l<5*o|8rWr3*f4FP1kHe9)R|E108plGfp(xJRhpP~&jF)40d3 z&hYmmCu)-lf}(M$)qHzwp8OKmp5mb@i`n3Og1kAlf={Toaj1XTZYMLC=yMiI!O;I{ zFd+XX80@V*!Z+d3D4Y(L{}>*tR~r6T(GlBjnW{(<-Q^KHH>p_PY&YNDuFL&A6})33 z%^UQX?We_`*HoG%wu#x;uX}ucvgxJ3PI37jb0E8F<5HvPvyWrl!=@*b^!h$@_!^ts zlPnwyavfh)d}vSO>jMA1a)-W|T&Zh)_jRj|oJjKQ^6uepKgv(tP$Az^%x@V~iq<=B zLd)5I3fFkc==k-9j2)xa{k2=mr*t3EpC6UWd+*ew@=RI6*L|;BRH@5pXX)WTuafo- zEy%vkPqf_W_?Lk5!bZ(r=_^Q0)NP+dmo<^V*PqJHojNzyr(V_aXSS&G)~8pdRyQeX zCl3Zf&9NP5s;3=QSJ?WZP`4SGE60lTsR?0Go(0AJSG>nv&<#XWNP3>{Jrj2G9 z=?-040?L+tXFfUUIEmeAJse58{&T&xXzHVDK7^bBa?VZnPeG#T7LkX~J-I*jSoO&M z5clhM4xMp6gI9QWEktijH6+J$Uz? z&3q=tcLIL>l8wInRPa`{rEVPJXy7^`W1R+$vR7RSi}DGc^Q6$+tSVsE%ZQ z@C{D~sob$HA3N?~N|(_R`L2me|{L>6+yyi-T9{21cE=tv?AU4n*zn zW=OBfce?N-C-5%euw%&1=B=eo16RGtL0jT~$_Un)YqvP&JfBKWFsvJ1RqB55K;KA6 zXW)~@E!q2$`?JS|$K#iFZN1&Hw{6@tBXh@@f|4TsGmWF;t}-IG9cagGT(-ND+y=UQ zj~{=K`PSu0*@Wti2xP&|iV2r>4fYi;3B7g&`JzHw%vWE|Jv-5XjMYv~iGFBbKvh!{ zYaVvV8?EtW{^@G_N3N80QP+U2>zVG!kr+KUt#zxEOL3V`H3SoWOhzqR^{I(}>B+%E z_nMyE{<)f-Hh2Ddx7Zkao`OB~qYv9NJuBB+g|1xNHyX~!Zp!HH8-J=Fzi@!2ux6H) z^ewgO(Joi5@gS;YUDyfpfwMKIZ&i6BD@0Y}#WAxHkkFKM zGPAzoByagpRG@~ZsKo5-&eOWBj$x_Y`()-XL{9r{TwRk@Q|!5K-g!BTy@Qic^<|9_ zI$^;@U5<1MgYp-eZ}^ETVr~EW!`flDD(_!5>2yi$kg8KH+h)sl#`YDR&-JX0>)E1_ zy&#yNx%ixSN0oH2>+15poW#++MLSFlNSe#_oB3RM7R2ZaM61(3BA=5UOL$Ot_^l5; zE-LZ)?ifUXu?^iB0DYb?u>=44Y z%ffb#$)Q(5$m~14M;C5Z9qT%YJ7|d~DqNGXsd*u2&>T3X6Nv(z`#Vn0xpR8zMsJ=^ak|tI;z|CK?q5iTxJ0{<)?>?cYoj?!X+(1jXA6U?OIkxiY1G#I( z*MdB4P5TT=DL*vQhAtRTY&TMdY7Cn9aetb|MQTz z*dsZY_S6d{JX?59uxXRW8G!+N-aEZEN9nDXf)w|!HKlAS=^}5Ft=(^GFn%md`nkG7 zlYkh1Nxr={(Zk@tV%^|=A!O@BmvGuIwdy7OA(28Z`bb>y*0g0JICt@`&g70op7MG8 zv!vr2VxC{B`P$e0@aL|Uu&$j^Cwuo-$7d6&gQLHv@$L|e?zVX$zx9Or==q0EwmVH< z)D`3mUDZF%*k}>D<=N0=(uwz{0%O%K4r`=FU7niV>Y9IaZ_8zb$i+;2I2XOm_V zjiZiPPzHOg<{V13>9ovOF+1~zT1et(F?R(zCaM7|o#>_Pso+VyiBS z=6-m)$ytV>qN>bj&05~pb2tuD`TkZ}?QX4%jU9gq=L~vQI)5%`DHz`oAHPC} zXMSFc$oQN7+82B5FXA+`aTa25lPMy3>Ywqi^+cgKSUYEZW`8LDz zFNgHOIAOPt6WhIZZXn8}E@_K6ul*1WK7)u-L__P zw!E1dK~m<;*?G=yRL2z3+~aZX(gsR2Dbam}s~$b!E99B@u;9d;KihemyeEW9slK>J z`or1g^UW^Kib^W^IrnMJ)AWC+;Al~0S8x)YO2kSy9fSX;C7khZN;tc#MSF+eyV`$K z)c@PAw)ClG>ihd*roGwqTb)HZMe%;Evzw7Gb2iR@A+kAWdyfI1;kmaz(l563)YjvV zHo0Fpv-a(_+mj|!S38YTjfIo&i;vVTN%&P6n%N_n(pNPcIuy;Q3zbpol+*b9?FMgB zSjdp2$MukT&*WrkM?TI?`W#pJQZBx`CRpUd*lnEp$3fc{4{)FRa+#rS@!o@OHKq94 z`r-b#7h`TB46~4*BenA4<^u7|Wp=YN&AcO)>n42-keSeQEf9P5fN7oDeRIzEkB^m= zc|X5(2Pb|1`o!&2?+HbNfmX97!9%t(&j`z3 z%rAy{+|>A>n3M+IK7fCrFu@?ZYn^^I&(q~H$Ge{94qP_7lUI&ZQh$E$OY2XTk4xXa z{qBL7Uz20Xf=furXOMp5f_$WM@#L4bRY^l1Ca!GikE@_vZExOY(vqKQ^(vN_U%Y{T zbM5Arv17w!FQpqg_3mgI%C043xyh!;QHQp;q<`QOV99q2TYySXov=Zf8|Hw2%d#=gMD7_~1xr5)$g8MBn?@(N>RN3_Xlse;xY zOcNUZbces(cQi!H}=51J;KjzUc7kmc6ZLo z<=(1ga(5P_dx(^Y+-AgyjqT6q4Lq&&!^iJxe`webiZ;)~8>u?H$CYZTatG#S*2sm4 zil##l;7K;=X6cvaoTj#P{qRbx7(d^U{Wxt!@S>GkFNS}H4Jkz9<>vX*MuXQorkm}< zXX0DlYtOCXcS)%@SrOB_c3I`|my%^yC76B|6G)fpk|zfmb82@T$doS%KSp7g^{bc? zO4g=1I4=&qYL?@$@(^A&p5C$Oc1F(0u71kx3fUH;bn7GJW95QDy5f$0SNUDWLxUWL z>ORF){%Gw|Jz~>wSjwGN;`(`hD4Ab8Wtn73BT>U*)1i(xr``(P8)`}XHM8pWpm(NPwX($e zMPBmKORDG&ss{R?*}b3!1^J~*m&7g;q(sE8lHWP+lsQeqOw}O0t}``BoHs@0Te@3G ziF%0jIA2ZLnjrg#hf9;cSw8%@=VZ~_x>8kv8cW5af_m?JqmaY+%d*=TK@P+(>+(;> z{o!0mx78xjhKl~P^b%$GW@IhT)NZwT}2Tj zSurD%3+l(!Ux`Z;n5%mINE{PBJ$LRbDT0x!MpD(XebL`5+TI`5(sz5VvDIL;{rVNw zZ=Tm@ht-w(;={5OW>wYsN`&7$ijyMLY&eHIPuyrB;kx`#zPdo>jr1&Di34~Ey>k9Y zWpSmNaK1%zmVWu9wLI~u`a>JBC6*EvhXj+%`nNIm7*}}9H>mK}x!u2QNsO{eZOmxjCiixTqB*atO@6H%|83pQ+1)?!$#YRW#Y{}eM|sC-4iTxlH6Ss zQ?z@yM13*uLVvT379VBS`5oKPp5VoMlefyODMVR11CP(qxKiqG@$JCF!|i*GUwu84 zeK1c(bymuVv9M612Ncx5wsKk<}XN zR7?wcRkYr}`Zh0oH*cB7tJ#WFmGjRgBJ(9mWYT^ueb*X*}&KA~gbitZ&{+^pg|d5tr#NWC+zO6BG@Obw!?yU#Rg^eUs`` z-=Frhp{N4&pyC_z5fj%}h7{KjwdjCa!IhULb#oKE!~E20o}O#1IbLL_g}1Zs^?qY0 za5Z+v;QTX(TZRqQ|b#3x$|z3>f^0%GSSKb>Us|0v>;=Pvu?HnLJD+xGr zc=5ufr^xHUHP&wowQMt!z!ph1XVsfZiMKEN4vft_wRxY+k@4+u#nBD`dqihy3F9Bk;=v-IBV~+&=)X}mCo+V!@^Lh=l zmaNfN)FQ}oT8Ka(>pHCTWxVKii~fGq#oD~Q zj`5%+^Oy30DZSU;%M%Io7Vl4)5heG`T$4%^lanB93|h}&xW02`?*OCnarsU?$MtsdOWlqPYgt8lNYt+UbgXvGF(3QOg4=jy zo!82%y$lH4_4T9ftJWvOKQFZ2ptt&dRcmsW^L#KCf_(SYP?B7wZ00Is-epnkm3du1 ziO}!P-~733wAdlX!Of3G@#>G?+ONXTZx)iC?=gO`qer1zbyHu*8_N@$TPBQRi2+w* zuH>43QSp$QADK|?bmobDNDXB_~8TZ(gTjK|2kXvV_a)eQ!YtTDs_~%{9U8N#OJ&> zx?jqB#r&I@ArdkAmtqTr#rw4QN@vSDr+ocXR!cv2%{_fAWI;ca)jy(^>ByX>Qr-zC z<+t_V%^IM*wcLC|_@GwWCatq!RX1g@cD$psriO*Q+UJ+OYhZn6%d%4G#CKw#weIzN& z-o#T`+U0yd-h<|BqEvD5*~8dk*^B88RRY_0P8d?MTCbhlwQc7OEq=3r%2tIZ4g2>P zjYK^vf9CR{WqVO{Zqrk}%$w<1bITr|xE8A(d%3M^vKOlDTTk5 zXKqQoO1&Xw_H0#<<*TW?^=3tJ%U?(NMy4$D6C!`l^lZ6L*IpL74T;N5>C>^$Pp|9K zNls{-ml|hawOi~Hgr4LQJoH5Aol|zXF}?XluNU~JV}x$15h|57vY0}j@7!NzN9MgG!BMQ6xG z?tH=m#4Js5lQ!!{!XnwSFLxvNrskCi3N`X)-E};1SloY1ZFrsZ!}yXi-FGNqqgTUu(mfp~OThW~Ysb5|q2l2#>^e=mJI z5P38y`N2rRx!3DfY!o?Ya_hy1wBok$7`?pIAHy52=ca87eSX#2VL?SixvfB7S^c(& zn=6iKm32?XRj#|3+2&E^Y?GW)b%JV{$?l{{SX)(PiPf2TiMEKyxyY^p!@ zXl&3}a?MCILni9>uy*3}P3;ZW^L5-7_NHv?Eb%%^w;I@+=8%2is-|Vz>(8;;A2w&_ z54vcJjjZ#J@HRr)2tPcOK2!*q7S=0Qk&f+4N?3ne>am8+3)@dcX0qDL?wmWySG@ZC z<3hLQ8w<-^h8<;U_YYA8?;I4$iFN4w^pH={F7m8xMB~DXZ|9conBu8?d&F{xj7SBz zNX*$}ZtjR6!Bs`pZ*HX8Yd($INmJHdZMd?aKV%jmUG#dA=GmcWS>2Y_NU++qADg9m zr1`2Vyt)PI{zx!3E{UMT2Bt3gH0ufv&o_m;{PSNM^&I~^JL=c0NXZ+Mc`Lqm*@pi^ zUa~TUotQKR8M_rA{eOD%N&1IZpIBnD?*&jw`2Aks-=yk)doOUl-O@&ev>?&LQb1Wh z8nbNse4+8Ft7g- zQC=vdv-M-3(eZ1&-3)m?`}fOM{55Vk@apT=$8DD`ylZTnXOh#H)#?&1XEjuAS!cZB zFVgOfO0?4*&iuEZrxF@JZ@Zfypp)U_pCEI=sS6*tSk3t5DY>sZ)pT`Mtmr;qKUG}3 zWU9A!=ZlvwU&0af`ExtVdz}KpJtHG_3v>=DcX?;5P{ZXUe=+FZS3sEe`oe_^1TC#C z6PxZPRCO@^7z_>y;(fX^fN*N*c(>+Q5QV1uD9Wa~T&;f%&(HDk@vbf@`-$+oHDqa- z>sCEqzecM1+D=?9T-5%0ugg|rp|*vewI3zrlGnbf?f>$n{b17hF{HD-*vZw^HS0*m z7cY4!J>-v~p+3Gh#@)ULyBhFmyXpl?hwAGEOsv$e-}l&^sN#Bl!me&@9q*Z=QIcR&ANv4uQfS$J{Aj2d=I;8-xzU2bMNKvZ=>`0pIEIn zzhLaKMEbk#6>|le`3ZS?q}z)ea&FWPUmZ{PgHrRfQr1}c_}-0K93Z5lvGJ(j>YI7j zlKLp0RJvWCl%~~xLj3MOTK?HE^1}8;LmIQnzC_qkI+}Ql?9QWAb6skAr)&)E+E~Sd zN=9>*pi;wf&;IR zr{oV&*5Ij&G>S#;_HX%4Y#%N!OSy3O+~``e%%I80U9^EUU#PtOB z^o_(x(F=%bYO@;eDyiaX-A@bk&UqD;@u{@+j*{x*vQCoBs^;3uO`@jR8Q$U4&cm)L zMmhx{J2pEln13gh@+76-qSG!_!&>A`0*$Ds;W%f@3DWX9*+|Lrdvv}oxNvu#nA&X3 zOlElC$=1s@SL7qJ-nv;54l0l5Sh%?1B}*+5Vi_5u7nBloeriuSTsxrg&@1|u$zdn6 z0un=sA`~;>n&e_?FKu|Q;g3eWrzZm23*4&e6)m4`2-dM)c1oFBqklU1BPI7c>RM8 z0XRRLFD{7X0}5(pCXoz9aM}V~S8^UQ$Q(Hl3MfsXAY||g%axS>$_40FG$Iq?6ty8j zC(+SWAUN0|^sgU&a{)MzOr|iHWbgqdE15)KQt3=&hWHSi3^Kx`p;1LA(`ZZtPo@&c z;CcpyWn9#GP@Ks$0+k36hB|@5pfXS`=>#g7N@Ei76aoz)BH#it1ThU^5 z_fOBJa>Hb%G}F<93C;Sz3b|>`u*jTY04%04J7EZBOblTQjT(JMf=~O04Dxd`9HTgF zGFyH~IH?lgU6RXvqM+r<18rC4kAvbcBjWQ?9hK4CxFInL?vOAtHl>L{wCOh$IS;fd(mjv5^7X08bI%U>Hb7 zH6|kn1;)X;GzQ8Jz@7+|NQPa3pviPP5EN=u3K^;w1QN;)Kn`dXBSBt6-GJC)5Mh5- z-C}^eB=`~|K)@!bz^I*}Zqe{0CV@^TBM|zS>A^(^6<$itgjiN2lc64Qd5vX6CIzh{ z*j$zk5l8}T1GWiSZXvSeU|SF*L)YSn%Qhg0hUvtzAB6%J6`1NQ^U;Yg53>;4dNe4& z(`IBp0tUvc&0c_x?uR*rwG9Hj2oKf{+xAKiOyafbrgF?79+5; z6#NMohd8Dk|2xA>N61Vn;sz4T=5QM~;<%U>%Cs7sW(vH|`r`y2MBzVBE%e7|EryPZ zT^bqC|NiGT2=)dYfC$l`pc28TK($7e%l=y>J*t1+pZeV2YPA%h(;w7nE+9l z(6^!d4j}M03Cu`=aHK*L2?@Xgp+^UXfK8ZWs2l(xPzcbpM9G|pHY4CbDijI`tZ9g~ z9vxbCIy#jzjg~d4|xi9)M&r2sW)8 z7$4>ksR%T{m|Z|VU<2q&xe!afzuOOO5@0MVHV|++g@}MH*=A*uh-4_V7|7b0Nr8@+ z1T8WJ0TE_Gup9Jes98}7S>wpiEWskOC+SjD$ka%R#iE zZ3P_3ScgV90fRhHzF{*ajfln~Fb|amXG$n{)69cGUeI*XAg`elojDHfU=XNwEQpGJ zVi1T>LxCMx5CvLC*cCwNb75fsX((lo(Q#B77!HLP6lg$E#^gdw;5~2yYbs|PL@0@l zwx4kBKu?J*DniI`mq7xEim;dlq5uw>R3h51!Z;#>1_GcUl|+H_HXU-AK}V;e5Om3C zdo-;p2BBRP71jW+a>qf;LGupr0xpL*rGs>k0YLfaD6lmG-8ZL8hU7(ie|QZ3f-Zx7 z0|6~D35qC%0NMzy(&$W(1c3(U9taHr0=9%qh2KiS6#xS?f%7GTKs?NV z&|oD)O{LRkfapaK4S~bS%#joV6LJx<8`eW8@Q@B!2H61Vg+h=8Ot6R-4j~}G$q>Pu zJ^~I2ARjx#;gacZC`>1#S4r3cFcv;j(6E`Y6JS6)Q1ei1S{pQ0LH3!Fv)CMsIk@fN zj)XNKB}qUHa5m=7#|RFvX`^v5%(|>K|4xNyA;K5}VwFN8!f`K#W z?et2keZqqy!@PXLf%3Vp1hkBTgCgK}n1G@;{nKw3Z9T%!A7{diD;t~nMgYawil72p z`g?=8ct{W!iEU;S9Enc<7bU_0nYkACyASAEsG1jXWYe0)jXEJ~=wf#}@}B zhlyjKqF9F#8WnmZJm8!^^d&Cq0bI_r|DaC(3j|Ys9|Z@_zfXi032-7p|M1gm!K(#O z`*(-}>GnGWuK~cczm0<1RZ#Lb2ny5RM?vH9_fZTg@~;rIYrn4r?DRJw(AOb<8wC)Y zK7N}BVGia0ZxC~a5D{S>{sBH=IMi9@#^83f@Zdlnb-bFtw>sPRTC0PD!SSeHS+S&L e@4wXt^%3i5w;o{;EU&>!4F&_JqGGbj^#1^XhHN_k literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/Contents.json new file mode 100644 index 000000000..75ecf82ab --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ra_miscellaneous.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/ra_miscellaneous.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/ra_miscellaneous.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1d84cabd239b39da9ed050cc372cdc1538eda083 GIT binary patch literal 11988 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 woMnls_Loz50@V%vH$=8 literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/Contents.json new file mode 100644 index 000000000..ed0b87dda --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ra_other_news.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/ra_other_news.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/ra_other_news.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e550a9c046d3e83f5c7defefc34923956f9c48f6 GIT binary patch literal 12005 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 IWmV<=4>neGP5=M^ literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/Contents.json new file mode 100644 index 000000000..b7c7a6180 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ra_planned_event.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/ra_planned_event.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/ra_planned_event.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7cbbef29db4bfcce8efce6e0f8d1f206cb300082 GIT binary patch literal 13986 zcmcJ02{@Ep|9=a`*hNGc+H`UX|4(zPqaKy#V zVUM@XF~*>#i<_^5H`)xXpy_7g>!9IaN3(Z8TY%A>J5fHq-VQeIDF2myt*^Z!(Cb+H z=DuS>tQ%%koSxg(?cVz7?EAF)BTrwyHWu4ve&uP~P^D+tUrNsevNza~Ldg7TT3g7T zGAW6B4@;e@TqA3DsnE2SZA1Nx0CmiS-8X^V#XR)`MVclV;;h*iUW(DVl+hN#dAn<$ z-=X7H-X-UBHh7yA2f0&B>lE(aEtn@y6)HcgeKnvQ!<7}`H5?Hnc33+`c2M#2+^ERc zhu)K;#>5_zYqJR%Z)bm52h|+4p*^p9Q#8c#qNJ3;Wm-f3Cdz)cb6!W0N84IA4%w#OqIm=4(WrlVFqq<-4|h!?jk8d|*JtN{ z`6}KhF~_79eS}=DNS@(*WO=7%VBC}3^03F_Nw3*oX5ty!d1nH5h73&{vcKPXFgO2} z(s!4eZtUbOP zosBQX8sBZ?&D)}bcPln?T;HJ9X2N}=RPTM_o`ht8i%*>x{0 z3l&ox%9qZecZlDv;p9thVA7%)gmk8;B&PLb_+35 zAxbQ&LD;HkzMzNhPX({Ngo^#!X?IzRNH9mym44N*nNNW$S}XKQ{vjL?4_yog0v^i@ z2LcoQuZ9D0nX@y}BI(bx$o%uakQRfIqil0`aqc!FX;fS6_o~dC|D_T*wAH5d$K(b3 zqDFUK$JqXXfwenNth^-lL)v;mx{-xd@sj@S5?$THoOsS(s|8PbWN-go60ig1va1@- zsQ9scL(jX9lQZ)R*4D0QNRKLVWIx_^>zsWqd5tzsILk#*O*cl;h&mav<>cnA$Eq5M zgMX1DGFts{b)3|PEDG1-w27iIFKj~gt%HhJO7>WuV5|1sX(hM%W~`~#sp>#t)3Xfw zmJ} zu|7n7y;@JU-_YzSwxlO-$d;%1biVkGE0T`NR_|+cjy=KmPrrOd;?2oTR{rw-eTiAK zwECO%O+!w_Bmupo?#j|ImhFx1Y8y{Ub6K1o^G_VIdz^ZX_nt6Z=U>!$B`;r~X|wIS zq!_{JDgFiXeUax%H^4#P()g3QMbzDPbzI{t*>%44WNLHGsNvvEIjL=}v3=vdg-Vt* z9zmA;w!sHI32cmNYc)NE8k5R+M{G?Retx zj%}f@8XxFf^7Tn!YY{G*VAokIWSF4)p<4~sc$$Aw*0|t&d9HkmiFZ-MS>X+Q@C|3o z^#~|(U$p@LNPyP4n$dv9q_%=yA6i7GA!)Db4lB7~w&W{2WuRR?T~S*@Y^7^AbBdJo zbqzf_$^H4USDVRmkLqBy)Qz-gxy`riUzH0fnCAx6kT|ll`jehIA8C=1iEvmeQ+o37 zE>^J|mdo|$+6o@oOd974u2gHYFI->9*<0TPm3gJQ><+cqMPRoUFJxn7SHAMK^i;$~ zC9c<2=wd!s+PjEfyhZ|h*5{p+VaYrQ-#Fe@vH9|$m(K@xrd_yN%Cc(1wfCMvKeAeV zXEVm0xmCVEQ7zHtTUXvr8}N`#?clry(Hf&U9E6Od3bS0x)7nmW_}g{1Tc!v5nDGdQ zgnS@Ws~mv*X~k&TRqas6J~(qIB5L%8p9P=xxoneh53X||@A%tX*!DW|CZe>$Gz0Abw>b&?>Y{j)}(&2cXN+kK9?RDnKCaSp4`iwdEZWZ2*II-X{tw2G~4Mz1}(KG1%u}! zOh_@JN64ymo3q(zxdlc2JMviPr6Z56=6YM;oF}D95o+7Ck)xj?rS{kOvrv=}tHWEi zyo_>71J!!t_PuXAxQ1RIkc~Fh^$-}$vv#nyu7!1sR%!6Yl(w{BNySY zqry$EjQs|WvoC5M-Fup{{?smlqVd`+IT`PaA-$H5!%j+NGkf>~LslGUyUMCDaCFhl)H3u{|0rVGA? zL*w2$9h_3Tr4R<9RR)9YGu=J65rh61ifE#!h?mW$&K zc04LQ+w?hgq$0zqg2nwo&g%`xS4+p9UpOaMd2PS9f5v-go4Q;qk*zIDXVtzh)Xrd? z1KzyPIVsv7+^x2rrwqrsTE}U}B|p4)$ZO*W>$#q9)2aM<@piE$F$*j%N_&j>Pr!ac zFW4QQSn3`m1g|KXFiNHHIN_3geV^immqSX(x9a2vPqoJ(a|`)6kyE|*zTwZFn0A%%dmv}=Nx@Z^py0%v7TFU3 zz2ALh_r(76N(pRh-O+@vQ^B8ic#WJl$kY$trZ#zSV`R|dLrwSh&sL!Zd8D16e`|Q@ z*l?(ur$^E6rtLf9*r8zVE1p+R*qq948~w4i(l=huDRAS#J1pPh--*m%u%Thi9QoK% z*17&qPtn@bkFP4MZxc%{#gbF1NT?u!jR(gS&8n1ImE^O*PiI>K@!CIUo>olv@b_=< zwH_HykN67HwALNq{PF!|_^r98s|MHW8m!da-jb8b$@XkqDXKE(#wz*%}7IHqza-8R>%Zw5(i{nDBqKT$BGzxnAsEkv`$i?)Bdk z@c*=Xebl8L{rMT6X8*;yky~qsENnvUyYO6(PQ3}6s=U-K<{!&{pz+r0Qc9?pbcp?! zjA~x{NL_Wi+W3XV_7SCKDzr*=-m8NZ?Asv z>?lcnZsyaS7oJ!N)ekLNji>}HrNp8{$>^G7VYtptk)cpwC+dC)?Ck{%!eQs%L#ig?FQ)0S5Qf$yl%u}-W z#IGIfmD-KzjzZqztp=vO=YnU3rPY0p26Ym=RjkrvCo{-xJ~qRuH{9(4EiM@dn z=c-;?op`jNnKuGG{#mJiQ-8E+XykBCfTUZ>yrSsW^pq*GsK_xP(KCEyYd+j|3o5Zl zo@=Pn`n)B^{26&=XY$HnjKjx<0r!SLneUmVgu^KPl9uZ}1$QJY_q=M2ORw&6#!kFE zU{%)_n&E8tbREmq(m@uM!5zX}=R0u5UP0Q;R-#T;gCW|H9?~=#&9CZqh6wJ=&9w@w z_8pa@m6wH|oSxNm@HZQ22o~&;J9hES!rACuaGnHD#Zawhe%55g`}{~@78c2(NWoYE zX)Q^u997pQpC1c3Eu<%|FWnY?PB?mff7kO|AyPDC?Yq0NCifRUyyxVNT0h7;n6jf; z6&tdxHK{#GA@Jq5+39m$O2^|uv##$;aQF5WDJ}Dp6F4?)qc}tf4b`x;9Sk`(nX}iu ztV!Jc)tA$uzHDy~nte9)h%-rO{yaVEFB;;ODm&2G&^HoiklrMo^KIbGt(Lg{`QWMa z`+AOwho~keUs`{Nzj8W3HCD^5Ld!g&^}11-QFc-;X+B^i%bv(w>M`6(T$dX$QXL-t9@VY@M97taPcUu5Zp z@$9QYG;Af0P`RfM-njU^5Zl`OSkQXtMe?7RM1{rl9%D}wyeTF-!gbJ<6cz_H~rT z*pt@w_oaPGQv`A55zRR$V*x@YN8Y8nmc%d}b1k`$Gg`46S9Y>bXh~{D3<=6`Zu>r_ zphVE6j&Ah2=bK(oYkgYdab$+ppw5qU^N^FA5u5R-%7&~g{uj?&>e(gD**=+jI`c}j z<_BgSW9RL@8#y46j@3(PPXzykTe6+r(y3Qk72db0lN~3(&84$$oum?}De)_`$C$OU zLEqx>i~cie0rMZuJPpsUUF#WDwJEk%=PajK@|F%>a~m8iWpg-77N>H0C@fWcQ$;P_ z$63RxFhEVo@Z9M>hQH{Oz%e#zVUYodC-mfMq-^h?cU7q8UCIRTDGbizlB zZuh!0wYPuf=FDtP%wjWtr)C@1e~j&0e6u2qmw+neQL9@Mn>XGSAvlCuYYN4uzEIl3 zZ#pD6Q2~XRhgeHGl6e8+v3mG>U9i+_t@GZI-Qwdj+In^R+-FW!`HM$JIGo)e`QDn1 zHxSGD7}_vWKux_TlG@c9T&17lt3^FhU-w}L*Fk%%tVEJPR76r)zI3rwvbtnR?rEOW z?TJREGk)p(>cTOp-FP%4>$Hx!b2tY# zYs_eJKU>k0NS|0;ZZ3x$y>QV5m@gnsT`xQ#lSAs(kCm7AIcLqS&|eWXex|msB3;&n z<45z2p2YOeO}?gj)pGG#XT!GeM#W8iWDy*&?@;GnwQn=QA$xG{;=4&>+thf^^a3rx zQWn*3tyTBYNeW(Ul4Q+*pOz};U$=i>r9RveID3pcHRRYC<@-a^`H{-sc%#mODippo zGq?PuEPfNS=Ow{ z3aYlVdxi&jCHUW_)d-6lq$>tx!N%{&3v+8lx`$fc$|j`7zls`78=6iLtf&s3GzrE`a*99r(Y*_!Z#;g=?yEW9bthtN1vKAk!B~PbkeNIeob4B^c%l&L@ zm`)L1*_+&%%r4s9YM>c@gIZ{x`mMf4RNz(n=fu}{IOHcqlQOuZv^-~`PlzxuvHQ6*ZVk&N! zhh-tr?&Chm%j*25_HAcb*rU(XCi&)^9d$0*P-kE0vbT%vMzBR}z7@W`{kRf=EJsqLbSlj$S&9V;;^j1tL?NB%5$0@?P|>V1MM~S2OMTO zbSyd&1|T22}SLf zGwtn({owL9n+BKc&BM2bC(q){Ky?ocD!-$JSv@|kz0S}z@DiIs~K6* z2nEXX4d3$=4h%OO&6;YNir90VFjQ_awzA^Kxnr>cCuDO(K59vg^wVN!$~UZIr#v?f z20xiyaYSBO_y%G7C+>*!wm!60eMfXebLlYVi%cPv>+=2V(UggSz)#zLR+4M#L|1)( z|IPINz>E4|{J6gBx-TVOYGcvg1f=hZ9RJwmDN*c0J^DI&ukZD3(qcn8yLYaa4*jU3 zcGzU%wzEU!*wzvY7a8E>XEoGc?&an+NoaDenmVua;A_DEpCRAHG1hi|(~6J_Qc`a? zs)AH^+0KS@mzsDqk~h+%dm3)PpPzI3tER5~M`>etm14>5HJPKEE7-ni$M&5^se`CI zc;S=yoA#b`-aWs3FrS2-4tq+(mRxyrp=$nALR{nAXkvFtla8Azp|>>}3*4N_C&yQR^luzj zR$vpK?`QG9JTNRL@M~A2@SYv}#WhSsYyZ+^i__5Uf3mOKn{B%mZ=A6niL;?VCd)-A zA}bdMGCMarHd&5$cbi@VW|K(q_j128(U*!jxf7s)$8Hv2R$CyZ61WpcharAP>m-dhWw+PP3{V7 zcx9^y?~5PyC0=UE(#DB$J-Ds;XvENJmwyLE=uN@)LQN-Ziu?taBAZ+HeJ7!P5s!Wr zj(qS-iHLkYe(lzK3l&$M_}vfQeu>F$n-1K0DSCF)spw+Naj%~Hdd3?c`#v&YA1JOn zK2xf4S)sTa-1}SHNounxKBOOcsrIP6F)z78OVg0E`)19&eVjW_{@1BqGPXbPjl0SH z{ZmO_KU}FBA{({t`tC?mwsMcHe;@Fuzr^x+_B_!{H*z#*kJm%@m{qkxdq_~(Q^iI}xFAt^Y7mp+# zmvX>Ycl9d;{oQ0SR_bSYmFj7=5AP-?U$*7veQIdn*qzpp++yk@s{8SgPOXMYHFVll zf_UTAA-0D-(U^vB$IC<6l`lBBgbL>!>_oe7meJ_V6rDUJvqM=$r8|zgkew~P(BFUR zZEtVy+qZ9fdiLv7b+B{yjgRl&s2nfZX@5aQ29*~1UA6n{H4Ml5yu3V&g2J(x!{wnB zugEKgXdWIcU8meIncJtk<)%FF#2qh==s$fVGibDGZhCsUvr~{d<5OOV6A~`c?VFtR z+v;dAb2npC`};Gd0UBIweBTsbL|nv~y{j4g{=Gdu;`S8W(Vl(K+}u3*{Dtp!VuCy2 z6ezSd$`(DdIUO!#etTwrtywKgZVImD?A_$-lx65%IhhH8>+~(^V<#)q%D!fE&HVH(r#ASO$ep?SbHx7=+iP7z?K}+|X`!Dx z?rCo!YF`y2`B}Y9!(BXb(oxygI$n@NA<9VC!LdAWvm2N4HrEtR!_rFy5d-)!sc!Sv zg)wzwu=BGQO5fD{@=i3U5h)ea9Bv&Uf9z$P^(uwRB0;4Nkw9X>)Z^z*$W6(d67@>9 zgX!KLH!c}vA1U9nUP3tVUPW`gy}5GJcS?%*=&_MBFT;scuQ>ZyF5E<>SOVS%P1v+8 zo2Pv6*iUTx=%eDOyz*NUW;o%Y-QzD~++J)#RqW(4ue&hz!=lY$KGpn<{?|w6 zM4kKG^Ct8Jx%E`Ogp6bxo#*k~D!DGc_Ss z4doJBQ8m^#xcb+;J91&Hu%%35>&xN}NO)gU&E3XznyDA;eFzc!ooXSn+KpYmQxolxju%1aAMOJh?HioMXMrYPlqekNfqOyZZOU z{E|nkbTILf(`kE6tC@UR>GLWuwLN}if@z~@VO?Oj%9LA! z?mwlSrEkv3*4**C`I-(8wSoGtghBVEm(S5RXPKE&p){Leo>(-^X_D zL|%IM{e?ZW^u;C8hA`f{fan*J%U+IPyix&j4qBaxTK1Qn56TOL!(&Kz983U`VX*WM zTAuj?;V^hI4h*Bo7zj@wk$@~191$kMWVD?-O5q@c)}W#G12PbO4Fv|bv;RPbfFTe_ z5RQU|2w)U~i3Bu+fglKvW6;4d>HJ>(Ph@_NL1YXbPk?~@M1%?%A|l*C%;A4|@Uini zF$t0}WHJSjz#WALbnrw11&GJM+?FuFAq5MQ3Fu`Q7DK?}D0m7QhauyM5KLhx3&N5C zgu~%rFt(^a1hNB60S0l7;BO5qnUXO`#xeMW@jz%i9>xKy&`pW|k4%7UMZ{8opCUShNe~I40-R&fh5q&7 z4-)_b;&6B}1qW=vEjK&c#IAD4*o~~TPc%V4r zh!_GEco<>?Jefd7q$FVoI0BJ^MdL9<7z+av;D8eoVG0(oiUNK}BogsN1SC@+A{GaH zi2|H~h+PH&uO<>nB(P#>8n7vydkTe4k+IkfMQ5K39A?>EI{ySL2%Dw#ivr-tfAX^^ z0T~B&Vluw?M4}Lu%ZJE7z#=j)Q^X>55*CQV)Yxwf@WWK>6875yFwn&^lrAq3iw8cx zgwhp5AOPLdb%kt0S04dHFMt^Q(AA6wi~)jl5$KE~<{*)NOMu7+VSv%dh)U@KAc~+H zmjE8XQiOlH#(u{JSc~va*X(Z$P+{6&2?Lg4*lQ`=NI(*XVIgL?krw$!%>R2eVaZLF zbMxDqm`u|j=q~qLGNxxPQvlPanR;R(Ol2~WMWzv@FAMPB_Q3)Emm-{*;+TsU`3FG< z^vX!zBqR?}@E|mq$(A0P6f%rQN(RXHBpd-$36RNgB$$9kLK6ZJ1Of>0R3x;?5S9R9 zlY-DBlYp6-pc{&Wfv`k80mKOtBw$Fu%zz0xL?naM2V+PC5_q2mfnXAj4CvW`+)9GU zWF!;`7(4|F{s(G@h!h+MX8_^wL=q@OIKUwmVHH4F2#+NrP6}RCB!k=lMq!YH!9*M) zF%E|DU>=}LBqP}YWKWoY#erQxg2s_ZAVDEY#p6Kr0*-`a2ap_)Dh7eL26Y4Y78win zr`Ihq;1>d~B?1U!6NF(z&!BD*(GUegBH>`*`b^aW6Tt+CL|6{dwTQ!kdbE_+=t`vE zkvhU$OV=R`f`GZfq6+C|!7lQ#s6oIPLW{xfq5=VHOquBV!{fn41ygjo`bb!?j;WzV z?GZr%{;kHvA&`NY+Fm3;LiS@Cg}x08dJ(4O7Fhw>XBrrY02n49)sE>0bg}RtWiXv( zQFH@M1# z1XRG3iQywm-O)v7_!21NOB!1AIM7%z)yVKn5M+z$qicK7gNYRKqEjPA0TANn!1zqd z0SLqc3B(V>z(_d)AgHGxUO;9-LJ8=J4uP=2E$R?)mL-VkQ;enbd5c=afvgO&H$66y z)k|)=w8@e;1B}7Va<>O4gYBjG02qi*M7pIg0vHmYh#Ho~8zVx2zLo-WNv%uqyQF^x z9eSWIQb!E6xFbCyEJi7?C%_nqtk=vF$ZV9tPGVL}-GX<!TF@UO`2loX^nEG7bpIc15Lks&aFC@I*JD9EBfTit59IA-ATT2ahCv;L zmx0KZSO_!)5Cnw5K-!9Vo16}@-DVQQHOS7O61$u!%s4vU25R3uM21F(UE*TpD%Y%8y z5roOe5(HAwl5v<50edh8B7njH3IP)Z;RfeD55Mfk$$K^R^ea> zG>pKKpm-t05(t4bM?}!zfTjl|UL=5VNCS%pM=J!QKrlT|A$(F`GSCh*5D`Q*@wEig2Gq=qC(_wN+KZMglGg$!~+ciZy|%j65YcfEKCG}2rv?;G~j3oXyO5VJQ1A7 zm6WW298|ssCszNe0|Vn$S+^Ohw(!lgQXwj=sDQC*r?I`!B1tFsgwBtBTt}m z645Y7MnurxQs}>-K`0qI_|W{k?HqhS`dPY=Qc$OP_=4Z{02H}r`hB-;VB?Ma*aw{V z7qR8{QGdRJr{iJ|*aZm_naSKto#uxu{}&;A0J5Cc(l>92@Q9dzk>8sx$**qXYvV?9 z`g5_rw}T@JSc!rHm4@*_6G5>gpaIVKAYUOMA2hfcTKo`zmFZ8*AdpM{gg^j;LWF#n zvmiX8w?7~}5|)35$bY9rgpuC!kA>iF?eFACNN@e;EE4eIzd>Y>U;hOHIquJeAld$n zRlq!QZd%R+xRwK{@NWZ(A*tl(K0UfvWvD? oFr?9d;Q`@AUs9mD1UMizqW_G`#@m-}HIPp5IFyvs?tPm75A$nDhX4Qo literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/Contents.json new file mode 100644 index 000000000..f0a98bc6d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ra_road_closure.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/ra_road_closure.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/ra_road_closure.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3d99eab81f006fcc3e0e10a0adc018cfbf4c772d GIT binary patch literal 11476 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 IWmV<=4>neGP5=M^ literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/Contents.json new file mode 100644 index 000000000..4b7b0bc93 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ra_weather.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/ra_weather.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/ra_weather.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e809855a942941a59868bb5aedd05403156ac4c9 GIT binary patch literal 17306 zcmcJ1d036>{{A+#S(P%BqG4q&TCMk8Gbt)UAtggXsU(^-NlFwYlCU+H%aEx|4am@d zA`yjXP(+z34QS$fKX1f#_I1v6uHX6fhrPPrXZ#HJ=YF1d?M9dxP0^+}A{L8cbJ=c7 zSFqS@U0t@$oHgzaY@PYDmacHH^8$t+9SiTJUJN|V#n#c`J80nKNVd&=K?&j*? z>ctj+e_-n1>gctcO=DQ|@6c2SC&%SpY+CgF_jykCqy_i;uZC{UZXOoywsx4=$ic_S z&cWEjcFp&PQ=FW=96ZE+>I>%#KwHu=GyQ5)Iv zJGSiFC+Bxv~*XI|yhH@$s!omRFw z`pH;Jl2B%P*k91LAj#@pnbyou%^~;w^iys;nR>xP|9(KGk?2mEYvAgzTEPbEkO|kz zzh1L`u-BHCGjB+PWu2c?W45JRi&-T45Il@b*X^v?Oa+#f>6zq18edzJn z-K)ye&skTUo%h$Y6sN@9MyEe_R~9OaXxS_<+dATd2f2z{vc?SOo>9U zL(88>*s@<)4zBj!ZGun4t1RM&PWRG60eQ}#ye{;A>U93Uak|9}ThEnj5^X=i;y;Fg zxv{~26$RUhX51T0$#1lEYwWkKmx009Wjj<&I29)D>d$r8RQ1=+3XQ+Ab*^{%olke! zR&96d`fTX`ZGA#P1oiB+lcoG~>Q6^sU9~eV+)Dp!`MHw|*19D&m)Ct>yHTT?uzB0y zhL=9+9eeD}KNg!$T7UM=l(BgpR~LsB6*!ibJqtg+S;?-cTy@^pmmiP6@Yfq+6`ylt z!ZbxmlgA~MzM&oKX1S%6l({A-JQ->{Z+KJ7x}LKVcIEnAeSEUuv1)6wWopm&bw=w> z&pmZ;fM-HU;i+A!I@SGMcS`fl-qdMpZa#X)crIW(w{QbRxFU5^mU^Q1( zIBoBUIZ-2%jm}5dC{o?xydCu?OYMp|8uWOSaEE2=qsX!6#+vLs_%LTg#k5IECu;%& zpBJqC{HC$QefrHlax0gIZAi?2S;Re{@0RvLgB}0OxZuM42J4Z>oj0y@3^Q(?JV(=j zd+G06EDYzFG%-HjN7kVe5-NiBt@gZulL95B)4p}%<(rlB<*r`h3CweQ$8yj zr$;Fr>X@p1v-I_X<(_?R7?0I>k)rtQ+eS&j=-KVvj=GpG`G>$JvDO*be37s#umxS{ z|1_|Lzl7~~HWSm|W3w}iyMFy2u-VO+P@lI`<&C!W=|A+(*nPGw-}FltmhpDhzi`YN z@0d3_{!-F`TID_g{_XlFA2qcsJUKa_dx!M=sVTFjt(^4SVQ^A;?~^?Oc5OcDclCku zh+}gv%s%6}MQ8C}GFc`MPZz%N8Mh#P|K^t4^Uj?3q+v5A|8hsByliXP%xBJzmu$Zx zbtX1r$hHK@o`}#6j_}c5|Bfr0FFlxkzTAD^eZ{;-D;*Scy)w=t2q60b6c`aV0E))0UC2ZLonJBmFV(I(7Qbq=9 z6W49?oAbazc~F7My{MJz>yz3$h8?_U{qkXtz+|1UFBcvhksBp0?Qvsqk@n7hx0|nT ziR^Z4QLB7tlg1%)4R^!-Uks;q(;QM~xPH^*t)+{k?BB&vNs$X*yZ2ikF)-hrvo#}d z)_qZELDJZfR7#}ByS#Ie*O#f-3<&Le#QBelN#{@Q8`k~I#o-h5d6lt-yYrWA&(6Ck zZ@DU{u==H%y?YCf{v%JX=lURCw9l(g_EW7>-{c)!FK=1Aao?CKu9?cxr8z-=PW6ju zi~RU{)|QGqG?{~`FXX|RV=NoD$ZKJy)pdVc8@#9V&t6M=Z&k2KI@oUVZLh! z_xAb^2i6Zv{cNkTr(^bAqrY0cit{$7v#suCzwzH>xO>KK^Y<%_O*Hi{7ev2nu`fRQ zaM;dd`9|N=XNum-ol-~S&N`g;$aq^o&Y>d@XN7)PA=Gd+9c-?+D`IK+kb!wqTH-{piD@|(@)xhWnkQo!F=O03$r_2e4}H5JOl{2{ zLrh<<9NTb}I?~_&>cCN_8tTH@{mnh3RI@?`g%#dR9j&r!iH_|R-HNjp?|e&$Y|E$B zqi*;O`_Q-XXi%zjR`mg`gr~3U;zz&9wH@VBR2epPsgZs1jg`||l$V&aCMc}RT|IPN z?wb+^&C+{6QrF_LvDA!*%LeWol72PDU~~WUG}DEXyA_RAX&b$9)T8%m z$7fqi+H)t>Ca*_CkkaFDrKM_IL#eHQv{)A0xxB|eOI5=lR&mSYk1C@U^y+3GWPLw2 zzi6w*BLl0;#x47dt)?Ge?(DJtj7iJ6ZfhdcK5xF{_hi{UhvA=$Eh6tO7YSWMk`Kx` z9?x|@95-MLwRg@Wr(68KJDqg-aWmwv=03B0zbLDtdF$Gs3A4Lt-K@AZLjUYU4<6tC zLDRJw)xa+g0!%X8jI3#;New+lesVB+rnooNyWhF>tk_Ui4}(hz*ZuPyIKw%c#(HbJ zf0SOgNz;D)MwLICwjI~F^Dwfs|EZ~t=N>ByNA6rYam3`lb@Ho}Tg~6tJ~Ue!ZGJx| z>QLlB6~Elo8_J_~t++QPxZBlh+Q+TjWLSH`BiGEfzU-w2x@#x$SHw;S^M4UVLl*fK6~<~ry;j9RhQP!JEE{7ea<39JKfy% zlx9<9ovyXk%ROQ7CgS<_ljeq&)@o~RZ0{{^pO_SGVH6_6k<|?Sv^RL3LQ!Siex>2j zeyfj!zl(Y|HKw~?u5HZ()8r^Hbh*hzLuUFu zNO^5ps#xxSa-4svob;+Xr=u?GGTympSk<^_I2cA{H|Af6*YQ4VlU23t^&mI?s83S{ zRIKc~L&M>MNGbe%;>yRbKh%#2Q0lew-bSZ0As5EZ*yHEn8$8wTY@Z9e4=H}F$x2(# z)(=;{u`uzoJ14&C>ICyEZiB;vvTvNc|GaX-n6~>0t6onCXqSF^CRXhqsyHdEomHF? z3HV)QT+{{sPs_OY-<0uAeM6^juM6p+yI$zt ze_5OPO%OA-dcWQ54(ocSRWn z9~8p(EuHKCrFYCM=?U4lDlg{jsD1P1O~S!-txpU#8EpF2QhiX<^_~5oJB~B50<~)9 z-WShhwJj`H4{ZO~R(JhdZF*hN#}8@G%}0-`Vu|xR{y6c-R;%^SiQA1|d%t_G82vDBH-u@!Om*Jun{ zH)u3=ssn*--%O$bf zgN|k{pJHE<`R?qZPa6lnUBddS_uZMh`M%X{HuFr~S6F2(e9?ZqMEksy`@;11w;u0Z z^hqsshTr2$Un}jWZj`y{%DrW5v%J4WA6Khg((6vAy*f8HLB0PNDfL;iW(C}T=5VrP z!g+SE20bvA@2lR^?&O4vTgJSv^+*x?v0HAU^z3)#kGE-`U+3i=c6aL^Bfsp|I&711 zNZHiEU|G<@d+|Yrayf0r^S5|vJ9@8(o{%T=)-3yY;5hyfo5;9qJ$@fndBm8R?q97< z=hcP1*00-X4PPO~Id}`*6THEERL5;4X@)X+V>zLz^8FRKDCO>(m-*O=j3e$b)(va9osfN5`x z=lKO2S_K$Q@a5Ws#(|F@0ALbgFuD zZ%#}lEwd}zAZlffv|3u5oA=lKHd@>?ZFt^{>XR zcoUTV>ixygnQL>>4eit9w}ksVy*&3x@XSM}GH2V@TzDVaFi<8Y)BSAZmdVyP04UVwXQ9{oK`QSFXiO8fBP0P)5n#r|iVeYd2JO4-N@R{~A;xo%vzw+;8*U zN9DZaY;{&xd8z2}rpA;FW$VNIw@}$bv}Ak+_?v`Ajpj?z#u*)QyLqX+IH%QzJEZZ8 z<0h#J`I|8@m50NWX&WET?hs8S*@A)?)pI)j--_oqR$rd?P2GCvsOOIp*yo?#y{$i` zZz!iV_d>H~#2S13zh=eBE}u5J-qm}td)l}G{U3<9qb^$ev|RkwN+;xo%-x`1+xzu` z^0x(ZZrzYsLhrHd`Q*UJl#{mOkFM#bGA^)Ad3A3!N40DHj!Q2WdhU7J|7?_By;87l zuV9MGrXFy6^;nj0XL5>BigC*H?1;j!#<2|#@4ac~ZqGGMmZ!az$K3LjiuL059MEo~ z)E+UtUPIjT$;r>#1Lu97HOnVIp!~3wYK-(2SE&# zD0}PT;;re|xuG!?JDysuEjSrsSuihAY!Ei`P-O&5<#L&;@R_CgM&-NNF_C+8uh!3> z(`|sR4^Js*jge~X*1jRRMfS2KI^!nY+da7b-lfG2N#RB1Qf)6!yIOH=g8pnw7t6-Z zwwCGD$QVoWVyZx=`?uuU63jo%>X7t>?HE#Ui)F*Xx1fNkCpMI)6s-JT+HJmdD{DfJD> z42BC$!__bFZc=37R!3dk1|L|U^~~givhj&`h9#~Sy^@_W*LMrPn*{oT9mi{pw|uCo z@P3*6f>UQ7?D@QJ+@uws>s#s_RLz4|={J4Jo?(!^Z`rj&Gj&W?$$U}Wt-8B{aN4k0 zJ07SR2{(?~6QBOsUy@?@A*Et*_kdN>wGR=8+;0`5RR^1MGlQSb?l!=`e!S&qSm_Yu zz1%W8eAFJ6!F`1V6I;htznI^^p@Rr|nwp8bJGtskW> zj1Lc)yv*oOwO`nVz&^&A86PUUiGtH7yL;AVCW?=*f4b(h?h6&}$J6%~*_AY1NyKDQ6+%hC*%Uri}qATXO4}KiW;7^l%|z!j;C_v!CL zNA&2hm{jv^R`v78X9WDc6|Ua5A{O==+?%XNlOA{%3A_Ehx7yr1+MXsWhsijFJ7(zSsVoh;es)l!;$+#0CUe$KPPN!H<9SreUlq%E z39+&oVp-Xmp1Rw%{@G(0SA}DEe0`=;z5LVNj#ti=6%Q^fKPP_jZfnb~sUN!6j!zo> z<<+ZO1-`d)=BVY*yvZNibL*DV-9n9O-(1jX9oSO*>7AUN6gN$wbVSOQly(Hkdl|0gHB`LBZjUKSnoQ>UK6*a- zF^W-VBVT=5r1*64zPfRB+`{t5Po5U~VbpQ6A1FLJpqFV8{&coc$k1K2G4dk^J)NER zVCd?X`|Gql?pCEW*U9w~72WRnM5BkSTy*8xC#%(uu?iOtZCmKOT_vP$w`WIo-np-S z0Yb6we*O5qBX8-iv@u|r_#QKQz17;B_4?}4J}0)1pS3cl?5(cz^nk@db!xKe-u0_S z?}=25U1N3TOwy&9HGgJ%1iefxfBV6A(WVMNDfJ*TQnU=!H%>G1kzNxJTgn-9Npw;z z!{27?$!@(}M_*J;)4X=i>r}iaYf?>t`#{sGwnNj5gmF$YIOjK6S9`AVAAZEOxy0`h zRS>7=omVop!!SxUwB~|%;DFdcv8gxK1O|Lm?`0A!c>=8vIy1C(%HsQT+*x5mXCX6R zwsK=~kI`{Xj?vqnY;04xTgbY%@oTR^ql|*bO&;m}*Q)c|xs#W%=?VIuI7B`3RAXWVY%jNekS=W&0V#R^s{(my7Do%%*u^w-V4teb|nlyszs<+rbRptdaMaNxYO z;9i5|W$1>7?z^qW+~SDtx4gA547F);zM!O8n&fH~xHB)w+@IAZZ8++p-a4D~X)*!r zyK=`bR|vn?D{bW!@xZS-QnmJj;%wI}QmOW8`0%wg{-C_TpA)f3tY&%X$c1Jt8=1_K zrO{ItKRLNQ;h>_QVr)au=3#%mmp7vXM=JU1lE;q0->Qy(@a!dtevvA!9a(jAU*nUB z?U7?5yj9t!$IW?oUCCgBGB_u&v0Jf@K%cL2({*vyYP*vy3e!Tt5bmo8bmE?&GYGF#p8&dXLw zyIi`gW)AhWuZ-^ck3DL9?|#(P={>ZgvZwEXmv6=@eVce!#dyN(p+{}U%G=l8u_2-NKE|JZwMc)3+|J3D%WJnM6t?*5 z?T={t;F!8^``T4y7iL%vyykV)tk27|+iP1d=^xNZE9qEwebJfdLffH2kK3IRnZDk--EvRTg|Q}uuNnfz)vn$XS8X+}-^WExUiOCkLQb3Qu-a>~Qx4xA zo=F`D4+&nJuKY-AX1Q7Yv8k%#w6C0w?Ui7j_~`JG{EG@{Rv+f7slN-i;F}pBgguRE@9dg!={Fuv&84YUPT-y|S*^mGsH|BiP93xEC+LC1O;4 zxAWb*f6~b4J>ZpL#leOiKHs`|D_!i^um9z#ndd*`C6g(gi7CWA!jHFa)c>bAzor~eY*(NuN z`}D2&_;L0?-JQdq+DGY+X6+4anp6@V&yjwWl$6BL(OJ{FG9x(ivG|W_H&@r5&o(%7 zj*e+58T;9lC!FxmXJ*#b(N%N0x3{#kJbkKc(dwCzO%GN{HLYlF_8z&+to7Vs#iCbR zt^AB+3l$p1Jq+1LFMN?x)znn9GbHgdzqlx2zO}V=%v_#8*j^`)ZD(^XR@hg>Mh&Nn?_hCw$9F7nA`JYEL{+OE+!%N$gV2vsy@JGSG?)* zqr3d&XD+%E*_^%iN?n3%>lcpzC;+wb`jGtrIrK)L~hM{}5+cU9AE=Zye{Xt8l4!`Ei}HD4sidbho^W zVIx^NHYa2&aRnb$pHgsT*vLm|#gxjd{G4-l<)`e4viB4e|79I!s2lITe)ar;1Fi({ zo`h9RFPDeKdqWC45w}+ZMb9B?4p>X+V{<}Xl;yU#C7DYR|?XtS7mU7#Flm2Jt zJkx)oou4oL#bVJ<+WGri|LF{qmcgCucuMhw0yf2=D2hjaABXN5=SS~<8s^6{O3dN$1Qf+*3&|*AN=Q~ibv^!<51w|e ztgZn?9Ga#jTrS&%#ltu}p+Lg!!km9%7>`GZX)gO0jLYE*DG6W1ra5Askm5^z7>gzl z#^v*Vh3v2>ERd#t1)Z@JzCp zxn0YFReoaMm(64UlO^qbiG{9Y_=iyFdPb5FTgIAc4kZ-8BAq^zh>;c~)kVlz0oe!* zv%oOKhZM;bz6jAvOE^M_K+L7!Ll{CL5sHx+JT6B<^Mpbe6xQRyf&5=Fnj;d4NkaVW zP2yTYbHx$_lL*@v^Tjl53(xSlLJ1j2h(P5EDG~A34~!2?B4&8&M`y&O1ek<)>-#(+ zI8(smli-54zAuPu4a11J2!yV6|Ae|W*wxjJh1GJw1vHk}H4D5&I$=VL)3uymu&!nQ z(z|PeSYy{jKW)L}v)1>7?sC+>b0T(%oDyMWGy)h~6*8RquL;2DpP1sI?vYgp1SpbdC6-Vk_!|hu7D|9f@CkX( z^g~s`G$KMIlFnW8!Ax8r6pg6IbprMy!cs}a5eqRT(+`nF5=Mkjwns`xZtNLrDF zAfmAsE{}&ji!iZ}%M&sEBp3?y9x;Ww1F|DlpeP9qM1n#fK*8lupfoHP2%k?pNbHC5 zN7H0<37Vsfg77FFn3riKLDd5yBq_%rT!9FIf*e2)z_MhWaHbGubOt67ifDu!1cI0% zTtccP$ePO)0TUy%zz;l>O+4eWB^(MR2o8qophOhhBtg#7LMD!Q2p`heM}q%=7y;1|k}H5jLLT-=tja~? z@kxOdbA()=$So2#$R8mfo){6xKw=3Gp$!PNmx6`S6@C+-u8}xlsuD^NAPLBqH~Vq5V%^v zMKxinCYKVyPXdmBNXYN)5Upei9!Ot^C`DJEkcR+)=lHa!GtB{_c!1dWSTLVP#X&nU z!WgOp5EoULDP3YA7rO+6(m}&dm=)~^U6BY92?-yNt|MjSCPFF50+2UG z0k8v7nmWg#VFlO~ z8^zuc5nu)E7ZZ`K3&bEmAXoz2#ziiHbx4*VBY8M75UmUZK1VAqJcOvhZb_O8kW`>g z5$Fh}gRP+`m^26XGMEs{gzoJW^kfmF&`1Oz1O}r7G#4&Ki2?;ewvcuR6(~lGhpI=I zl|(9J9w~`pcnb#}#2Wk{A}k8?BC{|jp-KTwVhrUKrGN}dBkfBMTkgXh#5SDGK0V%UPdcW8dG`2h$vFi2W*C*g^57J0z@i7K=wRXf%L-%)Gzo9JR^WI;A}j=m8W&I~GWThpN@dy-;L?wIO8T8>o3y912GXlDuGr&Vws_gdze26^9Sf zByGTJWbH^^)CrIicpUr;44I3fj35I7dPWxTIXparA_c9ZI~Gj(Vddl)BPLEkETR<- zJ|bj1n4VNb(hrIipMX%PB#h!g9mIYRcF++#A&O+D>3??;@g1RX5G)}{5!eW!go6^& zim*L#C*e&2q60@+a^eHK3MEKs5-+H83@?MIxy;c87$2h%`sN}*;Ta&vS@;h;Dnt^K z_=IvHm4aAKEWrjyG{WyBieWqO8G?|k0CIp~OdQ@&9Z1B2%!vgASP|%e^yBkUz8P}F z8kkm~FOUgZ@x>63L)lwgQV#9np(csOV5mm`DJx@$muYE7Wg%5MUFCSbX5(SkmcQoH=1F zg5hczg&H4_k%%)C1V}_^DSSX4KySfTpm7vw5)xn$=x|^tWzcM-6_N&iC&w(}cLw1R zT@6Ht{)8z=KN<&JVAu_XHIP-oOH6r$bn7%47YRcA2U{T}I`;>ygO9+7Pl=NV(=f++ z0T;87l~Yg-FbARoL5%<)Hm1P%5I891B)x^OF{!E&0csd(K9-I&ASwVVMN9+)q6&2u z`~|Bcm(fOohs4GpPZ}w~^h3Qx&>gzsE5-rh-ZEkb=1&1ciL$LMJn! z&Ql;lgbEO!q${onumfxunT;AzcxxXv#q1?^The5eH;eA!-`v|K?f>8 zf`O7i$!LXz!wyJ8coTs{5TXVN0_!t$4-8g-LwLvrRC&Ub5Gg#E9_%JTWn>1g+MYUOyqQ7&%3eX!`*Y z#la;jL>j`MSb#PXi_|{m3@H*Kw%FQ`9-u1(Gno}0Ma3W>!Urmycp080k{lQ^o@h^C zsHo%$0T}87#b`~S(3&DJC{U<0QksF`Mvo7f3JfP)Boe_8UAT&1FkE0Wb()~WLoh5p z!f7b!q$y4xWJ(6)qbflIg0#>O(awO_4?>lRP>>~s9A;pM928-+#UVn7eEvHKS|i8B zUqB2%I0_01fk9S424FM_i6^L5Z0jeCwpciJ(FsCFp%DH8VzW5Ob;{G9O^I1>d4qVv zm{5!0MPe#Y7ow41I8%_Nl9M|Q$V?1^$Z!pbhyqF+3{Gd_5?2E-AUuqENnAo=^#_LQ z1_{ZU&Ita2!lZ=$&=P}FB-SB3pu9To1b(7Kvf-u!Et%XVw-h9!phyUf;4nd?2ryU*l>twgm5U(r zpfgB5BhOIk&=Mzclp+R$&V&Vk{Y&2eKTr-XBVieGXY`*b2SklS0?8m)6sHp;0eP!} zNFl;p8`pohCBU+=XNGkN1cTw`iwFuFKj2CNp`e1{i8|qmP_keLas!Af3Fhtyf)jca zkOYF7p>xzF2EtVEBV#S1t$%J`iT(P1n_!(^ z@*xX0ak9s9@iK_C?3&Eb&6{-pmrZyA`E@K@UHvi^*(Mh9W741dH?;M#b#`<7duLw{ zhh;3Vw1m}33gllllshh*fPeo%v@@UBc-7qbL&)Ga2rsRFgFs(c8u{t!2=Vp%SU8{j z-U^ZS`&dv3FwWn`f;9ep5hCW!?r*Iie}8X<+m?TYa5Vq>@HmzHwg^ZW;@@9~2Lx97 z4I=L?eutp4e?VRywoc9t9xUScX-07SXiqm62Tk^9Cwt9K-|LvWxxw+ougupZbS#|w b9EgvYKVM<%;l+3jDpo+VMvRy|Ys&utwXjg? literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/Contents.json new file mode 100644 index 000000000..f538e9791 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "midpoint_marker.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/midpoint_marker.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/midpoint_marker.pdf new file mode 100644 index 0000000000000000000000000000000000000000..111589808d39a82cb08666252bfeabeaeb3690ed GIT binary patch literal 21017 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 F-2h#^9c};s literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/Contents.json new file mode 100644 index 000000000..b46e28fbf --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "puck.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/puck.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/puck.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e137c6f1b2c6dae2028bd745800a64732e99e37e GIT binary patch literal 19977 zcmb`vcUY6z7B`xN7J6uk)X+qFI-!@KpduhxC@KU*dXbJGASGa>NRtilTK#=2^#Zf4avjR!^_N*YzOw%v-fkd zv)A_`2ksCbaB}sr_d;4Bl@7R)eeCt@?L5xfBTs`*H8okheZ1_+ZY%-N+4L!0fg?Oo zJpK%PiNZR&xDYx=xpWW=*R2xPsxyA5bYWu-+;p5<`<0Ax?Ywy3$Ze1=WOX5XU0O8m zv9${FbN$xw$knd1Z!{JXDmIN_SvU9aWFfKpYG${PjEs-o&dkiT$j2J7YHqkf8G^$m z&P~JxelJx|;Rv3ak`04!pP;Km-s%}rcg}%|hCJa+-J0gVc>cUoAhX4(Q$7MY;fceq z+PLNEL)=3`paWw`Nsv%EImZ$>bKbDo2$bo}QzgZR^x?I`-@f(reRD=3v>9+HMw56m zt^K1H+R-5@2zpgs7LUmK+;#!kB6%flwgQMl$PJsV7Wv7;%{dX3doC~R{xkr8{P-bb zP8bp*Ix>3iYA#*%GhTRj)t>7OJeQAQ;$!qB(j5_}YMBZk(#F=shi2%ixlb`^WEtdF zU_QB?bCFAm(H}rW3vMT9HjzK-i$eEcc`R`UK=SCZ86OU5-PzbkxM_r!^Dku6>Yg8&{(aK=*Daqv z%Or5#Wjpc-S`(Tn_3Pw`9_74yu4MnmA6zptmz5Y(QX;N~EEoCu6j!9TeYu1Xn-sUX zA`rY`TU%{7b9KYCJmllI3~RPWl-nQXoZGdu>KVFBHE+(rA)%D>X56kN!;QhcLvL22 zr5z@Ar`-O)?xpc+#=l=WdMzpQlxo!;OFpRCQ_+>@QTdQ@>C(VnEoXnFN0go+_jr#U z2^mj$&a(-;P>C?!)MQx8PhY>RN!GO0hIb*QT~nuFq5l4%q0z8IYI z=Qo%7Cl~U9E*z_X$3%uVAO0A2Z-iCk$KxW31ijX0 zIxi2@tJS?r)mz$^esC^VDA(7n?vXK3bd{1AIW7@)iEYavcQJnLM9|sIP4a{OmqHg` zRx)j_#b#?lM{N#s#U1ISOv3I%&F<+eR5^1dg}S^v<|b6p#&oi$VtJW!D0F>Aj8Wvr zkE937W|l{`@CCSrG8y7Ty#1!I65r-Ll*NLgsKl%W8eC;2Ue0nz{KZDbdWr&xxzD7xhGY;Y{KX*dxVB*!4ByQzT2Zg zb8{+9A-o7E9pL13lOFZ9asN!|cjma@%ed(qbW%fcqc@jpm!c4nko`3Wy{ldK^TP}- zg#^(KWJVF$vx#mdOBkQA8cv9L?SJQrtH_EjFz zY^fNn|6P(3=G^WLD`Iz%V%dx)A=afAo!0dqDqj68=(Y7hT6FyRVe@G1nk0qS!LUhP z)8Pnuh|=$aRR*>Xw7*LKK3f$i`anAJBtn}D2Rp@e!{eFw`n)e><)etIXnCWt=G^h1 zv-lkDAf&(;1de2=RD8{MIqYPF<{ikK_oC}c*NV5)Va>-&{OjIF;Z0D>=wif!eXxCX zTg7}Pv-5{SWzF`LPJGAC{lw&=@1|Nes`)2v_oZBF2qIuk2^R2S!!mU;AT-Eb! z@~{nGgL@DtnQw$sMSCvwaqDhlx82e`8f7Av_17XDl@_iGd{iqXxYCR& zCA0}>jq_X_UK_9G^$LUMum?@Yo$4zmE55B9g4Ruh(=}J|=cl|0J+$yP*y>l6cyTVg zn0F4|wAW6y4iUzbx~P%Ye0sQarariO>Dee9T-`XVfk#!`R2%Qup3hFIN#-M)4p-76 zP_mWxHQYPN*IwDBRQe2463~3ZZX-q12r$_4{ipwtw|0+cMJ9UXls37 zsyeJ~9ng9t9Cir$0>U-ihk8jq%@L3S!-)P?SnZupvG8?izSm)Vn@$w1xBM}vmMd7u zCP+-NsaTKz`#cM&wfcF!L0*@X(gZ%R#kAS7xG+In5`u-FmCVmo$T~YPN9h zziAbxukeT)Y<=f&w2`rKdp5!~TsUm)-ssfx z=;!@;t%pJKt04`cGG%1lIA8eb7+-h_-J~u8rF_^Ny_VhZN(m@-w#ru%J2W>tcj}b- zx0*vjAmMWFeRbF(`sGEBobU!+~u?wK)snk&)7@enkhs^3%J7=M-F$9>50HsazZ z?~-qtuD(T!pI+$av=Jl#ky$xyj$+~ zMk|x1KF1PLM`80-O7COwCev3zEYO!T6fteCu#M2GN{bQ)dsg~WJ*GwWI$X%fP}c}j ztW#!cr?`nF`_Rw*n#*P$X3I0~2{O=B&2B0D8C1^1Rq*{g^1Lze31oyhRu6Kz241W< z+ASteX9Kx5#5{+|o(wvq`Rehc-8p9MVb9TX{+H=Qs$oOrPHcgxo9R zg>=|WZDZRXmXL4mAVb8$2s0jx2X#z`jm0KgIOp(!ZzsC4#A@4K(48+`vt>~UoWGy@9_gSz3hZG#em-XR+()P`mQ0PFA{^jwbogas> z&qWYH?o2!vAN5?Q<6=P8OzZZs7aBm<&Wu;Ae0!CbJ=(nMS?{~u8dqWW=bFG7vEX?R3 z{N=P*b$v8HLv_Uza@DnytQHcXEb(0JyvO-(zoa&bfYs@$;Q zu9c)<=k@D$r`=0Oy&co|bNj`F2Hw98#X-|cYpOd<2d)&Clbze&eHpOYZ!&JR@X$19 zpn&vNdhvaT&WgX9?N@BAvrV9_EglX7F5lnVEvDlfHfL>CbD|QH-O)BiaOP|_x)rc3eJ|eFazXZYWBPOnWdhR5HUUi;*Q};S@ z=FNgy*3zsNSN-X=BRAscDa5zmgI`Av3|?a58s2655!ugL!uySv40xve0h{+a5<7#7)HgaStjpehu04PF0mpEXlxTi->q&XJRdZj~ z?7&;iJi(VTVN6TkzkffWY_8s~Jbw6X?DradX%R|2(w&O>kazhsRI3-v*i^ z*mP+|cA^_!`lZS}*B|Nc?dh2uzE>l2ZRDJ#Bb&@*G8F&twG-^4HW#Ew$-0^ttY4jw z9o48e$P8uN#5{A)IHv-ygt6mBHT-!JkOWOJZQ+Px%t?ks7M6!*hqaV}bre8=l zpR8Yc*n|yUap;s@?)?31z;Rl$OA{2w0cO>9s3`ld2c@3_D>_xwrjdi*8@rBZF_Gxj zjwc+mx$EwDosMnj1hlS#1xWoQd&XOivn%9D?3b@t=BgTIm3H(-9;e^Si+!ppPUX+0 zqQeFkKQ};(b71k>J-Q5x&UayPB01a!5L0O6*1)FH%!^~epZv^Mrj&Q_YOUy}>b4Ah z4pZ!eYPnPx-AMoaYfQ|`$H&V{md~aZSHtcYwmb4M%WI>U!yTGcvhCR52GK6gU_zzy z2Cwyf&Uq#RW6X+2PY=oA_ou11^YJ0e&#fPHEjh21Z@kf2oO~9=awGDC;|wFijKby* zyXsgEz7>_Kb)~hWkbHs4l%LhRpzDJ}WM~VkaAAd2qyGDd3vQ1sKMxKnB>j{dkhR2d zq&GfX+9R2B$ryHMfUG%mxmOdiCAS$;s}KDo%67h%XRC8PCiEIItz=+8^OZo@XJ}<0 zJRtv6pUkd>=cg32<#?~=dol3YK)GKGezx(v%y>8F3G)4{%h9oHKJ5{$YW~X`&YNNi z8$ab@cR>U2FU~!a_vad#_CbntGMs6dc>1e<+UJ6c+|5s|Q$0<58|u| zX@@TTx>oL@(+xch4Ix*5i+wtXuK3wiaU>#FHoZnnq{83GsxkWd?=yk^g{l3muK5X< zpGn3WWJA^(Pr}fU-i3o zZ=j$hx^(k}uTR-kTYegRJMrZA`#^bt>#m2y;~mv+9ddZG8ZMFRBx%5KAC@2fLHhlR zPNv15`VGLKhvp@1^#}D5m_xBM8uE)uZS74`(sqgwU&!uuA-$SZF?~FMCnmEZ~UpT6g=^JD5LSZ3{)meKD2l37+k7( z^3j2c%zl^DN1`ta6Xx%(Wk;0Tcqa9KleKi!%PR@MWPaMqJ<2FPvNHO!Y_Qui^}+EO z>5kOLON*?U>6{AHLeN#y{5JuHjuj|7GhPpN9O@+NEb9vGJiq27@LhU6O9r{-useBDTTlDK%bX$qo9+5B2l_n*7M~lc zGS3T+wD&n9Mx{2Uub*{t3Y0>eva)XE%o5at$>Icq$LiKHJ+knbPXB}`{KXOUI5YLV3%hIf@rciD;(}p!;$+IKJk^B$3Dl6~XK8 zGqXGre)3l!qY{Jba)lxLfc7OeAyYH{ADgw>uhaXXoB}KB>S`9oLOJ$g)fW#8uG5jncKFkPg8zfhXVC`!0OP}L}UnW08!!Zv}&?9$co zNSI_~dzg77gHO}gQ)o{BrF3OnAd#^utdK4@OdZKU5HHbV;d{cC--PD>@=Hq*5!<#q z6=wKJdq6z6ynf;04F;Snk-N@R>=K88?34<}id29diewE}FyXRcDMF{27>V<$$j9mt zVglNqhtn-$Yz}N)Xd>PyVDwOr&$*T;npa{jFJhJp%WSXztvt*Huc2do4at?~xXl)e z=}yS>!B(h!W=}*{%?Sv2($(%`KcyO3FKBevETHvzMQ^syG_%JQy;C`%-vweh)-R5aEL@!<2(@BP8Qm#*h`w;WRwsr> z^-{B^(HFzkP?=Y9(y1)%1_$y;$R-V>=a9GQ%~ETwBM_L=-P&s`j<7d*Dt6h1p?0r` zHhxnmPF8n;w+XU*JdTV9s__{g1mxLwwLo6K2{-b=n%{OH&byJl47BO?mN;ePuz$rv z%{==A^QK0$Be>oi-)%wS%RP^>*t?(Ixr_wws28d%)GN^K zn^impkD?OLVC-M;|F0LWxc|2cSFs`t;G?Em)_tf{{4|J@Djz+rdc#wQ0x z1YV!l_pC2=UEK&dW$`p@5XJVac04n@@hEnAHI7y7Xd&UC+fk*2FV6Iv*RHi(i+Lw& z`m@r(LL^}L5)=4yV(~}y){|6mHXWNQOlQnD3Pf$?c-bDvSm-Ae6v^YT&J;Ci5SEz z<`h$N&52G;Pxf+0jl7$=CGB&MtA4*jT~&8g)eCPt;b@FNiV*iZ?4`Wet;&45%b~t{ zYjf?UvF6C?Y{ihKfDmH_`QdQr=4v!ga?R}w`LFUzm)^P>=r;D&JF59)akx#?T8C~l zgsjb0xTbPL+M2jU90OiHj96;ZMEKfi5cFUEM7$`xo_4F-nUuD%u{@-;HdDGKh#2M# zyLy$Cvo44%=Aaa=F%n}h#PsD-`*qa_-_xA8``a}vv8R14+k6ExhC;VCl!t=tu55gb z)nZdQ@}7zO+#*|o#Q#l82rB)AQ(I2E=qs0Fyx;@7-^?0keUE$KEh?(l7n*@w$F)LN zMwpgX7boP2N0@UMg3Pn(qD4a-pBL5~EczPb`^qt=^as}0M%dcvW3ccd^n>=1&KA|j z^%q)KfAv^zEi^0Du~n~pVTEN-ey^`iWCuyWSrw#R$E?nJljP|p!UH_+1)o?v&p&zg zeUEZl7j>!nYyY8(6EZ*H^mBrByoF+g{c`YlM0>)W)p1`^$QhF$kvD~|jnRLS6GADXUqmq&OuSLuh`JBOYR z(7|>gg@vScbY8AnOZVHN-8#|bmNyqC)ko6novtkHDlX`iHqI(L7+Z7mNN=dHe52uK z`19u(Cs2ByZgNn~ID17K3)b#hf>xVT$W#!CdLn zg_)UiO!@)z>Kwjh^k-pjUuhjvdaz#q?22Mx>m+Ax_KW3}dAq^%_dRIR32hNF56Mj_ zOd+f*xZa^H>r;E;ke!8eUu1iz0SWeLnZ6<1M6)bU&BAKn_>tQc@1=2OH{ZXQpycf5 zM(r>CVqx}0Lm<8PUE+j`XrC5~kbVH7*+$p{I)A3*Mr>@>BxQUu*EsIR^9NOY7Ytkq z+&d!;+<4y*@Ea5X_d{(KMrM(@*{~26p1O6z5^Qz$0>@Z99ZuBr#qvJyyj6FkmE8K( zq+iolmg+V>n!4SYF5K&KI+^E%vq20mDcyFqp7}w5>wccBA`4RWkm@nb=y>he8dOmA z(c2f!^8-`Wj};CYx_#Uya=lBjvg5cB*~d)Y_ar0@Ui1Y<| zzwS^AvT*f{&vrhVj>o6N7F=|-_6!;y=~R9HQB~x6hjzV8@Qq#yqRlAwxO`@IF>|4{ zwOjM2YF^bxxARMGtciNP94h*nCd~N#j&X_N^PBDk1ZQ|aS+hx_qsS4S8mH7=vED?~ z;5h{`;>Grsv?qLpC&Tn70wuLQU$DjTeV>*v+0A|Bmp>s&Zm6-*ETUQsbeU_XUJwF4HX1>XvULJ)F~4xI)(*`CaaK;g<7b&&m#-)8CZdis)$c z$fa3Nv-uiJHfIk+X}Kk^h9>C9I^g(_Z#ht>dE{as@5=ix%^v_=i5wpaiM$m zeM+$wUEf!v6|(=sp4krZ!P|olj){xyv($A4q>s`2v!xYRuDcf;m|}i-x@g=nrHae)5NnfH_~$wv7@J!|hu`6{*obxS zYtci3(pB^YN&Ug}Ga9$^I2xBvzukPF&m4k$vHpUdG{`E9{iqru+^L+#grhIkS!B&& zK0Gsw48KWP;}p?f+aF^Mqg297Eg2W+&wH5M2#TtAd|8e4S*>J9t8u=;k&5|9QZY#K02@0CEuK?$2H-iOJyNJw}T*Sfq`>}?*;N5{Po!>34jTZ-Dv-oo$LRC|+>oolyzf|Mq)kZcK~hL*Hw)p;ss+g?D^fz; zhZMl@D#C9`et;#@`i+NUbAVNf+KpBRy!BCw<7*2pQRDlD5HIwC6f-CSm?# zvg(wy`Ck{#zI87*2tZJb&P%;Y$f^<}8yqfg(HS@@`D53;{rwxYN1Ryh@EJIUFF0N7 zR3x45pf|F-4Hrqp)4Mt6Ma%0wS>l4k59dC7KQ0z|SvfL&SnqLI(JNTOyjfV1diTpM zmZHW@IkPR_8m&OyM`w4RaR^}ZH8sW+iXGu+$ze9S_?7qVnFy(3`p781;w$3^;hkvDG zNiOzFrehE3+0RM zuitkceb0G(fcXwf-Nw;yU0MB+BBoq(sh7Tr)f_E~P7Gq#@E=XhGEk$hc*!$&a-|;P z%aWUz^9pM?$+4nZ$=3`l-C>dzt%v5|$w!V>WF(x;R|woVdC!tJAIBgoI}mu%h>8C0 zX^29NNs6OL9o->oQgb|llU1RDoaJhy3-d8=_?p|)Sah}Xd~Wo8q*X;-jQiuwb^ z{i`p&UF&e%H(+w}cwZdqBYRq-Jo{!!14mJLagtl}C2vc!NSUAV0YcyE5=%o_UutcR zJ;L#om>`2#WlDV4%T(DCYwjOy>$rN0{4VvG@jfn}7bB98M#R6CG)hOLf`j^*W55D_}YxoK85qVSYA81}}#Q!@YqR@mX zIA%ck?ny|jW@-mh@=Ycj+v=y@Z-+lInsZn&%o0P=_zt!-w?{xUtfApaTbM%;7;=D` zMPp;(Xl1qY2fx|lDd}yW@DY7UhHwI_h{A%;>q0S`XoDqLBL_EI<)8u{*GzwQ`SW*A zhTYW9GR?C;>~ocuAu}4|A^0SGT&c17ht`v|=(Co-S>de@KAOC>7;N7o#nZV!Us>;k zP>On}dsz4bN%;;B5-GP9kz6X4dqpI2Qw||}eEeZvV)yygU#IGdMt`01O_)GO-p@UN z`?xDSpC`E;6};aSBU+ZZNVL?XWT;0**EzwVXP}~nmYWI7;#W@!BwCbI@a)O+Khaub z9kgDnf0X4TOM~pE_USf~+|A*GtU3Dys#uoJl9kJ_9qaO$5GXT~g1MNU76VbvWJ_mA zqmV7G2skz(ujFfudLZm*QNx{>cCBmI3Cx-WQee$OcFA2z#BRDQZvy&ZgEi763 z2KCDN5VqApAxl2zDL#|TO}V9|pVfv>qUXzdf_EQau3D8fl~{?e9{Kj=n3s5Up-t00 zKeT!wobmj-;~~B51pmllT(#JH)uTc7%~K6`@+CVGWr_Y;3?Esgs=e<(WfU4WU&?L{ z5BJ^u&|YzDv+RxTQOaZvzZd`@LdyZKgfg1UWwVsQu;#^wGu z_3&Q-TRN|JgxGIsFWO5TOwnsqyq1xKv?8UjP*eip$9ak{79#DQDo0L}(hb=Z$Wsc1 zBI#-k=MB6InA7)NIzZamS9%%3cW=Kh?AZO!s;Jri#^hE_s--fjBT1(3{v*VyEyXt1Dqd;2x|=& z>R7nL4AryH>351!58ceLKJAnA^Un-7C^$(8^zM2k<}#Nb3lH|MqND;5&h+nD zOm0itI$lAIsx$RJQLdM1O$hU5uFoiPTXkBEJRz87dX#h`zvxiP2l}Piucz9Co9BC4 zpCsgR!>bnq5V(&Fzhv*^qE4*JYZYR|Y`%OiQLf2C?(NSt6WV=)aCX(gVye_;F2Y*i z-KCXHp`(>EPjpDV6_l?}-!JzNbe~@Mu9eFj;x!*Z8Rg4U#CPa*aZ$=FNOQ$Ko=!F0 z2ipd1niyy0iO1?|El`|tVF>MXd;=Y6OZ=|TiOQmclzETPLC8k;+>1wRmya>e=*Xz0qc`e#w63utqn+|4N%uN(g-(dQL+>|i zrn}(3s^DPvphVDJB{NOrVwT_4+hR-y31yM^QA2YEB`=Z3Vg5QJAuM;V+LSX)+XOCt z3^pDB;wUk#HpRu|S~?n7?Jw_%+iMGhDRRPn?vO2 zIKoz>lS`Fvmw%d;IRJ~h%*H-uXpl9I+izf8EXL;hSx$RR#->*(L^Vh3cl9;J62MH2 zA^LpYrrPPf3-MhoLLC$_wyu1k@Ly;63#>3lW!7Y8{802EF!mxHxaVo|E(WD2D8 z+t?iqP{c!jzzf|c+kWr0hN?w)CiCEjM1@ARoApnG=;m*f-?Sd*9#2sRUV^cWKrb=X z^?Dae2eB)aCnX~HSj@L4yWA547Xy;}p9K1Ru0DTE_S!68J5Q#L!Udk*JtFg7>sm$j zV@SExCKLBEb+OP=^>tjXqR1FO%hw6sw>+@k!(42(&59vvcff5?FZ&o|wdp=NJfA_W zM}EZgyeyPbmVH4yh+$o=Ndnvzgzu{KbDf4;XN|3B4S)49*4bES1PF-kxqhNY2fF87v~v4z+)NjGb+Ii=<7Pr3Qe#&a?9&tRK=VHN3D-K8I~w*dnAN2{HQ(bz z?l2v1m#?<5tfPRT!!4hgve`S`qqToZoilg8K>lj`gOd&~+sGMps!h)$)!M6{BLijL z?xL9D&q}o|AZ$BMl9_|(#_RM5PcK9pJuwX*KpqsSlT98+vkEvXGxW4o^u%>)eB5KX zg?H{RJL&L!yl{QJyfYROeTc3#=}tU*vGE%eoTs-F)) z-?k@27ma?BuQph1zBN9<+vypN^0Up(bl@;p+$`rkqqN}3sZs!1T zQO7!?a6{!D$@%$gC_k=A8|)q zIs??!YhzAUvfe#nDkH@=IM>?1=ge*Mjp(W=u^2%c zF}T&I&avfSVg2Q$gLD)zW2YQfi+d~*Z0{M{ZhX2_EWP&4b@}*XJ;ZOu zBd@^dBzmUHDV!th-2QLovD!o3^DH*~9o`6!htiG}MeG;92pw7JI~?Z6K3cE3TO=9> zCpe(Prx0y#PI##N>W?hvgsKLZ8;UXc2`)bAWx<@>Utd(@YQ&%BMISjf&)l&`OX$<@ z$4&U0p zR&f^A>Iw^qnG7-q-(1F^4VxsJ=>!VGv#%fa6$}q6x9W=<6lXVTcWt`K{<79x-mv86 zAVU1%lk`k>r@FXv?97hv^`kFFgBWo4H#>icTHY^FiYkBl#CYTJM5M#4yg~hQm^sOe znY^Gx*5zN3t~2SPSL1q4O1)T@rKD%>gJXp)#SZ4)T*rRiJA9A_#{DFZJQ=MN`xKEj z%xB}0#`x&=YVLy`tX=o~d<97>aQ4U7udCb~V@&UV)4zpM7S4_xXgSe7Tup(Qmqljw z%D#?->|*v0<3lQUa~pEKcz1Wqf zWLI$d*^SX(^HV(`oORFRAVLrQ^cUMLHlLWLS+pMJ+9fW%ivn@@qyw3ugRW1$7Cxbq zy24y-vsp|^)m^g^XEO^E75`9d@$y6DUirs#DmM!cqPjk`bwf0AM zh5CQCLT#C`W_*8bEXFj`EL87 z_hb+2hcjrgJ+yZ+a?$B$DX97SM%Fs3e{)(Ew-g0mEatG|bCIiVSADdpHpnGU3=}Ye zIwkUHa0wM43j#!37R_x9!kU!WC>fOG8pM6Oh+T|i#LcIdKjBs~JK}~Afvy~_%v!7= zQ1OSh>?(map#d=)EU!r}X~F97ZU;SSU%tK|jfSGH%c86=x6a5lPE`-i7&k#FH$d(32~_F>+7 zm64I%Vf4*yb>m`&w*$&fcXB73kH(vwY;zD^9@;z8(6E0wbgf$QtHt5^%cgkT;kjbp zu%=s4sQCq6#t@QGZOB!GwrBXwF)=-s+oxsgyjlaHkqqyZTBhpQ>+_Un!TZ87(9pV)mKWwvOr3fYc4GmGkHdcki56|PS<(ftA zc~_e#c0~IzG0=uT*)@}ZmMjt{@^q4TI}2ZA|L3e5Q^otk!a5;G8X#oOrtR=%*K9`q|cGET>!E)>2}a#MxSn=2`P*LAYMdwGc78vJk$=`#g#%%_FHC zaV}TVEde`%d?`hbD1ajhRvwNqal+v!mqU<3QXsrPd{_OHPE0)8^wJYmW`!>ES&e8= z)Vh_98XaBx{)5xVN0IZAdeL4)!mXw&g=IQ&k*K83EL(VT^iZqiteVFaYjrY#eIkur zsx(BsHW5{t9OUi9%Fx^W+>apjMzes8$M%)G8IP?^S7awgMkLBbg`L^>(^Hn^=TAA^ zcnSr(NsgL7mxQ|Qw%qJ_PN)=NglA3G1XX*^tD7bW%1NyvY;97J8k$XX1u2*D74 zR>5Hw8R+7~{1VdrjlbG(AoTLooe*cRaTGj4s(EFrwD|JF8B+g%MP!eTz{1GtV{-%kE3WT~-PAs67QKG*D|F9w`!=$0TeI#KeR2Ac zb!MaMgrACXjpxgFq$JqiHbsudv(=KKa|57eJf@kf?5q9lMVmZXS6i>1??3YVn7!@0 zxevGX<<-+BCwLEp8?BzK%0v z4tLU(-qR z^s+B!=)PcQAl?{Hqznw48SFf$z`mh=)7R$t;LN=;`e|h2 z2Vn)dx9tTh>et|AXzD{o8Nd3)--(Rik=?S3lL-?Qg}D;<>!$3(eSVCT3?vwG*rkOOJ(;h8IH7Z;WL^Hi_|a? z>9fda6+gE($D>8{P;p^qX2VxxBmT~1*#s2@(laqEc`p6 zcq2|0Y6+wCoOPc(^ZfKVDH+pO$0_!Y_|z$9&_Rl~wTvQfvwzBI%QYa(lwQkq8-Wu! zT9dfX+U}DJkQmO7lLZ3~r1$pR=h~QC3PYuHB>#=wb z`R01}`SPRXeVn%!5u8DFT+KvBk&P>^%O%yMQlrFDk>tyXqm+*Cx%9EQkbLH3lzfjL z>ntPl?a~5<`ER|_ljiy<8CSuT?p^qT`ZWhGskVsNH$9BuwRCGWPmOL7zm%8Wko$PK zh#tNXnHWHQ>cp#{zMfm6m)nfon-%AIXVqG1_SR|rt)5Flx|)j?{O^`bTk;IZsaBsd zPTweeb-$ssU9up_Qbk5V{Cl9a!6F}Y-xSEpK7^{T=pU-PCT356f$5E0=Dkr!)j zUIh0n^xd@}LUe6YFMhW5vpopzw%=A=%uqvv=VcJhle01f=B3H)<`j!m2xdQPnl(GHxyT{608hU*)gRk%CXKhIqe{%gU`*Ho2v2>*PMbm!}*;Mpf6 z$&BT1|7YjTa-M~NQ^YD0RPcbkB8v7xD*W}1psa{R;|a<*Dn?L76P58u>Q`)ucpM6a zv~y!o0)pvzup9-9&a3UA5B^I89B?Q_90pH7qmV>J3>u3^sQ?cDLP=$S!7Hj@0Wk`RQ&z+g z2t=R)kO8ZRp&AX1#1RxR7$O0OrwZ{ev@{<7LjC_F1zwS;f>H*?1S%7VDnu2`wsr&p z9#|BKBPuGR@F)yb;(r&ykp)PhjKiv6ceF!e{#9Tq2eh)HG8RW9V*c>OQg=fs0*hdP z^Z$rI0O0>JrI~lzk|@HTy=V?nQ3M799KhG4711~>urO8He{&laco>BN?nGh~l`%LR z&=7dyf0T1jC}q^&*t-3Z>yImevaT#d5G=}Q0_G1>>h@?AMLYpd#Qj&){2NP} z!~R7B;B-7K2!S?eoFWlNK;u-97;5CA!TY~30`lLu+Wor#U^$V9A}SMs{RqlHJOUDf zQ^ccH&=~aJp#L?U9Si1PWTeXeFWz%wp(Zai|L{mW*at%d4#O&9@kA6*fw};N!K;A% z{%JP5e>E=%1t2M&Dj)_$zyaX_f>S}^Xb}hU5m=sz5Kw45jRvEF2kB1bg~O;QD{o_< zzR;*}C^Rj4P#D4vLd5UTV9_8&Y3uO7@7wFJ)KsKOpn^jGwI7WW0k2G?g#ikSCE&N$ zftUiWp~{I-0W|=u12qi0y$%D?jEWGk1Pq2ogHj<7w%4g(0SYis&6I&p0F?@XNT9Am ztAJd`(hwjDl{cUP%y-tIRj^bU(4ZcI5=Z5(LJdT!1SsHC8Ui$+`Wu7763{z#A!7bm zkAMZ@(d5MJPywN68vi4ww|#>GN>h2Em8m*|b-=qSG%H}xm~Bo3V1jJ~N5o*aGQ* zQEFY>u?qnM46-e!GQg-*z```opwR^4wgfozj<2ZkxFaVH2V!>H#(ZR3 z5d+*!-A@_B4sAb>K--@A@7ikjZyEQu+W7Y_vt9IoJwX?xDy2*SQc}wuHLACzBT_Sw z27xw7qXOaq{p@IE5vz;=t$14t(6zSdfM7HTsEXZzFguX4GH7@^OF>p`D*_CM-`SPg z`lx~cqbTn{MBH}5;8gyz6vXMa_h`PN?W&9d(M$_GszYfIs4BF0`Qvvg2(lYAZEC>K z!h;53c526V3~f_U4e_^7qw%A5F&apVNE-Cl7PPRXLHKQtVK5-Nso6vWG0NM;VLJ(^ z5C+s9TAWfdW!rLp9|1rO+FlAWi577fYBAV0CqQ-r6C~%hnLs0=#R)ZsL8abaibfH4 zN+3w}ZJVHoDCO;{L8}K;K|sZ%S(sKfRCbo4u;?9-R$aE~(7>=X=P2XB^Z}Nl3E1sy z2epKjSv18!_t`0_)PhEXl)+d;wH&B=I}jF!0lk$9?G#5U9cT!|ovQlBi8#==X?{h~ z3N4iikH%rPxq;Gv+YuO#2Qjm~1+{F`=#)`AbQo%Fr$Jz7qq&e;e*c&W#2lF2XrfR% z1x*kz&20-pYbR8QI&#qh2TTt%=luJqfCUpGnmTH1cQIu!kO7ksaKINJdqMJo3_#-k zWvJTm_CKdUFz|s+{Lfj7Hgw{Fs|kQB7Wfbg3JVg96rd+4gLz;FQvp-Jzf4r1SO3od z_}Baf)C9>0hDabX(33_9N-qdADh8SlmWBa+K;qLd6;LgJ7pRnAN}yU0P*R<=Ly1c6)H`$R$8Q5+c#bbaJG)mCDcPMFP9AKcAQA;OvHI|46pVC&N zK~D$66b%IhinccxTR?Z+#xOXrBXukr5O>Ms9@H82$X(fhBcM1zn2L@J2h?fCoW zu?X1wUowi?AkVS5(@v0ooe}@9&yOf-cws;~P>+Yehtx8l0#qmBkhJq7s3YJI3!3|X zo!!uic#yv!m2n_}(RdKF)S(*y;8&ggj)Cs~&yydh8X&Cxai9bz7MvpJFVv_afEgDY zoq24Bo<8UAWcDi#RIvBV6>oy^S_*N!C?tUow143V<2^MM^Sq* zI4gk8fu^2+{=(2;X2;`jAbkFFLbhW82PNtZO`Wr7`{1aDDp1wHoC~7#k2n}bGzvJK z_(x`Ca3lb6MFhD_J1Z-L`U~Xy7aRV^c}H3P<(7kb5wU&C0iu-n*QEuxAJ~DZw;Vd| z?jAngNGs}(UxC-ouXUOH=U2Io*q?PG>v{x$U(5RIUgEDe;P!%cXMsWzzBN+e}891)zJH|9si>_iR?pm^>Fy_wEj4N101&}kwc{&qa00d1s3&`o=-HAAF+S46P#OdH130NGUrUv#Q zdgIU@V1GtklnrMigkx(=>I$P_QYlxRazmYx#vUd-5I^G3IHQpBG*)ODEtg&nMDw_X zOOG}yw0=-xF~FFx4LlT$jMM!}7=-6)hNoN!TZMFU9}_8Vb3cq#@IUO+xrZb1UG{+_ z*Q16~#y!``xc7JcSBRBdv*`zqHEx`~zHsqipUG3ZTH{V>&BD6cYcctTC2;C0XWu^5 zyU*k8B>tWu^InV0r3!`HNeo&jyNAc;fJ72@FddS`vCNe($aSthsC3{ z2>yT#geE{?02FEmrsePFF7#Eu@^0cya5Tda0UO#@I(mQ|Ag4`mCwQBAp&fAmy)fE7 zP(bl}ST!|z4SvQ@`Yp!K+9`jZC3l8)OgT+@{XA*K1LSmYzIaF6S)|tgFKgCKzjJ28 ze9;uLBKhS3Q9yh}Fpv#+bxNNXWEx`-YQhv1A>pgDM~`Q}lm+(O3r1wLsG}|q`_*vF z5Zf1Slhy`P9bM`O$&GEX1hG7txzlDfHauN5H0-<>G*rW2vKb`R?WyL)Nid={UjTTt$iy~ZzAC{xEqovs;Dit zmjSFhpr8^g!B1sMF{@}1klzJ$uB1xIM?vNJa@cYoicU0_xvv&X9k1<3Pm`F;5wz+N zcn-fqsx=wS6d%tcfpToq#os&UU17nI&lzMgP}h$2^{wAFW`OWyICM`-+%SE ziuuCCz?#ym{M?%}ZbRAS4ugNb^JFY2#sHJQe<&SK{n+lbxm01cfq@=RX*xZA&EJ@^ zQps^0!XH{jxT(u1Zsn2TlDJSJys%(3IOVcYKRk9M*lb6&J#fo>Nn)OIN0W5NYRrB9 zp&XfyVO7lf*5Qh}RC9((kG&ey@rS5IfZ@v?hRV^~%SWQz?UR~6a7ePp&Ck30FlM{S z9{;Ggl^ss#tdCM&%jo*FWpOH>Up?$E16~+TmzrOYZ=}=R&B2*Q!S1Ya?@qi>#{a9M zN#=I;3;w0ZkLIQyQ`jP6Txug#!YpsdV!;t11LGlw6eL*$g0tLsTrN2<1Vt*FR2#6g z6I2b!HJe)HNWglF=gJ7= zjYYUOFdewTLc9xxhsi>iWG?Iz4uyF!j{*!4q59m+H<@MineZB*$C_%d8Niy**Nnbb zUA-9k!vbG|&V(y??X?A+f3eStog);f0gh@gf-sJT;aS5!bMxkEKDfbmk1IoKkXy)_ zH9%X_M2UyFMLXsOcap}&ONqim(pMkkXq%|6{6!S3VYR!ud6(rS0N=aA7!H%&O7O%G|4G$ttv@ zqjkeljB)=_w?xr)-A4`K`<1L=s|9#wC{gpG%MOEQWJ6_jW7F$svB^IDaBm=ok&5@W`jTf_RzyCMSJ-gkI2-CQsxVcMTG4rSZzJTwI_#{# zeNr;1mXxpzpL#}_puANm!`tsZmrC7`_LRPoDw^t)%9xt)z}TEHz%!8bz~cd`AL1(E zigq1#-5QWBj;9n*{O(Y)9N@c?+tIV-H8FwJo33A*W>sgC1v3Tf1O)`m1=HZI2!Tw` z%#KWI<}|#m(#4wn3CNmiJ^jQUlA7_U`<#<_MVxX@N$EntiC(pPBgLvEj)fJL&Ut4l zdJftp$=WDtR@qjW`Dvkyuo|hU%Uj~T!_=W)lirdn^TXz zgU%aw{Odttaj$fqb>68~CC8^1W0%w>Y$VEKvJ8?8;yX^8z3v}9ccF$-N#Ssta*==+ zBWl}n%imd9J=7b#tK6j0ekCv9*2Zym)i}$zEWQyw2bl#KvlDYNB34pXdn`RFXUjL6 z-ZW>r**ONaMY&EmR`leLOg$+nIx*&3zYugP)2@7kul7tWWtDSPbW36j%Ay+WA5G?5 zdrkB`RvQsCuz&93cy-SbX@W)avgD&agMzUvs~)MIeLZ>X9Q>d8`%fJ5?!7o(E$p2r zinIF}JuA~LQmr674)kiu?%Ev@W) z;`y?wq&3W<>tcc0NwpZYc(sFdzIDYxa_i_FrXAnSk>#P!)0;zED3B89A=^jrJ&+#A zf<=v8p6$S%OQ5C(#)jH(5xG|mVz&K!Oj_I8zL6?y2o5{mO`k)uGVq5I^8wZ7{=xo! zpc5Ef9qr~?{e<>c?Z~)zJ|jLqy$)oH?f~+%?q%IHU86Kh8LNs(J(4vE$XDeP5#qfp z=IdRw_G-i^dSdl53;4lBQdaPscCWt>d;6L1__>BYJhxn(bm74N47o4JMdSnor(edhGBeY_cfj zMoOL(tf;r>U-*d1^~ydiadd3?aXXl*Bw4Nro(Ivc>BJ9wGC!EB(VJ*|%*V>9HvRfH zg7I$9vg%PKU64$qBqV~OCFGKcK|lCC!al1)bj*>0XUkAm3lTzvcJdm(}mx z$V|<$Wd_E|e)whSd%F^9_m|u+Rki2m>nzYV7cX$(6R-)D@`aDdhGnkEfr8n4vtfb> znN#H1%T)_mZ`$4t)sK0E^fAUWx3H)k;Sqiw(!OTmR-))J_Z0A#jW3VQg!W|}?m2uy{nqZ471C-( zW<{s75hbyQ_x3I2Y`@?yTCI^waWVYSC(jerYAPpRWtEnfj&_BmmDw*n^DcTz-r6=# zD|RkbwmTJ+6+~W$+KC@LQhVp7!+bD)=E7)wG9FL^-ZksW*!(K~hK?q>;c!D=nY9NNIf&k@ZsFp01_# z7H0?hZIPWZ9nGKJdgn6K4Mz*>ls5-gJ*KfAC;R;Hi}?kyqs4XVt6@7iTaGJji8J%4 zbyRA0yt>~Nw(T$e5*|)dV`~x1TmPhLI)#2mX$7eKFIc7XD_x^Ou7S3;7TO1g1?T{K z7O?v@g^t$0nD}q<_W9o|O^4)5G#-Znaz=Qp4-KB_;7*J78z4jJ=>2CzZ8Q<>PH_GK z^*%po{%@p)LVvC}b{g#npv(YE01Ac?Jkq&ckbd(s?(HqBTO@dQsDIPq&IwF67GTJKE?RFbo>v1XTn7|0zG(&4);9jqe44(ee)#7uQAUf&UFmPOLou literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Sounds/reroute-sound.pcm b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Sounds/reroute-sound.pcm new file mode 100644 index 0000000000000000000000000000000000000000..c089f4d75bd4dce86fb6e829b5346ec786e6b5c6 GIT binary patch literal 66284 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݂@YLHE#( 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 literal 0 HcmV?d00001 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ar.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ar.lproj/Localizable.strings new file mode 100644 index 000000000..bfb547651 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ar.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "وقت الوصول المقدر المماثل"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/bg.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/bg.lproj/Localizable.strings new file mode 100644 index 000000000..d2782f0a6 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/bg.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Подобно време на пристигане"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ca.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ca.lproj/Localizable.strings new file mode 100644 index 000000000..08d80612a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ca.lproj/Localizable.strings @@ -0,0 +1,8 @@ +/* This route does not have tolls */ +"ROUTE_HAS_NO_TOLLS" = "Sense peatges"; + +/* This route does have tolls */ +"ROUTE_HAS_TOLLS" = "Peatges"; + +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Mateixa durada"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/cs.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/cs.lproj/Localizable.strings new file mode 100644 index 000000000..56a1b8f84 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/cs.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Podobný čas příjezdu"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/da.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/da.lproj/Localizable.strings new file mode 100644 index 000000000..7c5c42e7f --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/da.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Lignende ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/de.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/de.lproj/Localizable.strings new file mode 100644 index 000000000..7245b1e65 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/de.lproj/Localizable.strings @@ -0,0 +1,8 @@ +/* This route does not have tolls */ +"ROUTE_HAS_NO_TOLLS" = "Keine Mautgebühr"; + +/* This route does have tolls */ +"ROUTE_HAS_TOLLS" = "Mautgebühren"; + +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Ähnliche ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/el.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/el.lproj/Localizable.strings new file mode 100644 index 000000000..681a66554 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/el.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Παρόμοια ώρα άφιξης"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/en.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/en.lproj/Localizable.strings new file mode 100644 index 000000000..ad702bf05 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/en.lproj/Localizable.strings @@ -0,0 +1,8 @@ +/* This route does not have tolls */ +"ROUTE_HAS_NO_TOLLS" = "No Tolls"; + +/* This route does have tolls */ +"ROUTE_HAS_TOLLS" = "Tolls"; + +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Similar ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/es.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/es.lproj/Localizable.strings new file mode 100644 index 000000000..758bd1e58 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/es.lproj/Localizable.strings @@ -0,0 +1,8 @@ +/* This route does not have tolls */ +"ROUTE_HAS_NO_TOLLS" = "Sin peaje"; + +/* This route does have tolls */ +"ROUTE_HAS_TOLLS" = "Peaje"; + +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "ETA similar"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/et.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/et.lproj/Localizable.strings new file mode 100644 index 000000000..3cd52962b --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/et.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Sarnane ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/fi.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/fi.lproj/Localizable.strings new file mode 100644 index 000000000..b31de362c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/fi.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Samanlainen arvioitu saapumisaika"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/fr.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/fr.lproj/Localizable.strings new file mode 100644 index 000000000..83a27df2d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/fr.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "ETA similaire"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/he.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/he.lproj/Localizable.strings new file mode 100644 index 000000000..b4bcc5db1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/he.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "זמן הגעה משוער דומה"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/hr.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/hr.lproj/Localizable.strings new file mode 100644 index 000000000..fe645876b --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/hr.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Sličan ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/hu.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/hu.lproj/Localizable.strings new file mode 100644 index 000000000..51bcc1fcf --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/hu.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Hasonló érkezési idő"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/it.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/it.lproj/Localizable.strings new file mode 100644 index 000000000..7a365d371 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/it.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "ETA simile"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ja.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ja.lproj/Localizable.strings new file mode 100644 index 000000000..96e71f337 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ja.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "似たようなETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ms.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ms.lproj/Localizable.strings new file mode 100644 index 000000000..9552061ff --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ms.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "ETA Serupa"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/nl.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/nl.lproj/Localizable.strings new file mode 100644 index 000000000..2fa42fb80 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/nl.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Vergelijkbare ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/no.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/no.lproj/Localizable.strings new file mode 100644 index 000000000..7c5c42e7f --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/no.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Lignende ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/pl.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/pl.lproj/Localizable.strings new file mode 100644 index 000000000..c15c13b2e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/pl.lproj/Localizable.strings @@ -0,0 +1,8 @@ +/* This route does not have tolls */ +"ROUTE_HAS_NO_TOLLS" = "Bez opłat"; + +/* This route does have tolls */ +"ROUTE_HAS_TOLLS" = "Opłaty"; + +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Podobny czas przybycia"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-BR.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-BR.lproj/Localizable.strings new file mode 100644 index 000000000..47f1dc2c6 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-BR.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "ETA semelhante"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-PT.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-PT.lproj/Localizable.strings new file mode 100644 index 000000000..47f1dc2c6 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-PT.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "ETA semelhante"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ro.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ro.lproj/Localizable.strings new file mode 100644 index 000000000..86fc33b52 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ro.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "ETA similar"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ru.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ru.lproj/Localizable.strings new file mode 100644 index 000000000..931874105 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ru.lproj/Localizable.strings @@ -0,0 +1,8 @@ +/* This route does not have tolls */ +"ROUTE_HAS_NO_TOLLS" = "Нет оплаты"; + +/* This route does have tolls */ +"ROUTE_HAS_TOLLS" = "Платная дорога"; + +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "То же время"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sk.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sk.lproj/Localizable.strings new file mode 100644 index 000000000..fe645876b --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sk.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Sličan ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sl.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sl.lproj/Localizable.strings new file mode 100644 index 000000000..65b2b1f64 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sl.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Podoben ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sr.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sr.lproj/Localizable.strings new file mode 100644 index 000000000..273ded63a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sr.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Podobný ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sv.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sv.lproj/Localizable.strings new file mode 100644 index 000000000..c0d84e5b4 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sv.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Liknande ankomsttid"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/tr.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/tr.lproj/Localizable.strings new file mode 100644 index 000000000..ea0b15aa1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/tr.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Benzer ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/uk.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/uk.lproj/Localizable.strings new file mode 100644 index 000000000..d18126f0e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/uk.lproj/Localizable.strings @@ -0,0 +1,8 @@ +/* This route does not have tolls */ +"ROUTE_HAS_NO_TOLLS" = "Проїзд бесплатний"; + +/* This route does have tolls */ +"ROUTE_HAS_TOLLS" = "Проїзд платний"; + +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Схожий ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/vi.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/vi.lproj/Localizable.strings new file mode 100644 index 000000000..750b1fcc4 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/vi.lproj/Localizable.strings @@ -0,0 +1,8 @@ +/* This route does not have tolls */ +"ROUTE_HAS_NO_TOLLS" = "Miễn phí"; + +/* This route does have tolls */ +"ROUTE_HAS_TOLLS" = "Thu phí"; + +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "Cùng Thời gian"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hans.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hans.lproj/Localizable.strings new file mode 100644 index 000000000..e6fada04a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "類似 ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hant.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hant.lproj/Localizable.strings new file mode 100644 index 000000000..e6fada04a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,2 @@ +/* Alternatives selection note about equal travel time. */ +"SAME_TIME" = "類似 ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Routing/MapboxRoutingProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Routing/MapboxRoutingProvider.swift new file mode 100644 index 000000000..7313e9398 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Routing/MapboxRoutingProvider.swift @@ -0,0 +1,253 @@ +import _MapboxNavigationHelpers +import MapboxCommon_Private +import MapboxDirections +import MapboxNavigationNative +import MapboxNavigationNative_Private + +/// RouterInterface from MapboxNavigationNative. +typealias RouterInterfaceNative = MapboxNavigationNative_Private.RouterInterface + +struct RoutingProviderConfiguration: Sendable { + var source: RoutingProviderSource + var nativeHandlersFactory: NativeHandlersFactory + var credentials: Credentials +} + +/// Provides alternative access to routing API. +/// +/// Use this class instead `Directions` requests wrapper to request new routes or refresh an existing one. Depending on +/// ``RoutingProviderSource``, ``MapboxRoutingProvider`` will use online and/or onboard routing engines. This may be +/// used when designing purely online or offline apps, or when you need to provide best possible service regardless of +/// internet collection. +public final class MapboxRoutingProvider: RoutingProvider, @unchecked Sendable { + /// Initializes a new ``MapboxRoutingProvider``. + init(with configuration: RoutingProviderConfiguration) { + self.configuration = configuration + } + + // MARK: Configuration + + let configuration: RoutingProviderConfiguration + + // MARK: Performing and Parsing Requests + + private lazy var router: RouterInterfaceNative = { + let factory = configuration.nativeHandlersFactory + return RouterFactory.build( + for: configuration.source.nativeSource, + cache: factory.cacheHandle, + config: factory.configHandle(), + historyRecorder: factory.historyRecorderHandle + ) + }() + + struct ResponseDisposition: Decodable { + var code: String? + var message: String? + var error: String? + + private enum CodingKeys: CodingKey { + case code, message, error + } + } + + // MARK: Routes Calculation + + /// Begins asynchronously calculating routes using the given options and delivers the results to a closure. + /// + /// Depending on configured ``RoutingProviderSource``, this method may retrieve the routes asynchronously from the + /// [Mapbox Directions API](https://www.mapbox.com/api-documentation/navigation/#directions) over a network + /// connection or use onboard routing engine with available offline data. + /// Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). + /// - Parameter options: A `RouteOptions` object specifying the requirements for the resulting routes. + /// - Returns: Related request task. If, while waiting for the completion handler to execute, you no longer want the + /// resulting routes, cancel corresponding task using this handle. + public func calculateRoutes(options: RouteOptions) -> FetchTask { + return Task { [ + sendableSelf = UncheckedSendable(self), + sendableOptions = UncheckedSendable(options) + ] in + var result: Result + var origin: RouterOrigin + (result, origin) = await sendableSelf.value.doRequest(options: sendableOptions.value) + + switch result { + case .success(let routeResponse): + guard let navigationRoutes = try? await NavigationRoutes( + routeResponse: routeResponse, + routeIndex: 0, + responseOrigin: origin + ) else { + throw DirectionsError.unableToRoute + } + return navigationRoutes + case .failure(let error): + throw error + } + } + } + + /// Begins asynchronously calculating matches using the given options and delivers the results to a closure. + /// + /// Depending on configured ``RoutingProviderSource``, this method may retrieve the matches asynchronously from the + /// [Mapbox Map Matching API](https://docs.mapbox.com/api/navigation/#map-matching) over a network connection or use + /// onboard routing engine with available offline data. + /// - Parameter options: A `MatchOptions` object specifying the requirements for the resulting matches. + /// - Returns: Related request task. If, while waiting for the completion handler to execute, you no longer want the + /// resulting routes, cancel corresponding task using this handle. + public func calculateRoutes(options: MatchOptions) -> FetchTask { + return Task { [ + sendableSelf = UncheckedSendable(self), + sendableOptions = UncheckedSendable(options) + ] in + var result: Result + var origin: RouterOrigin + (result, origin) = await sendableSelf.value.doRequest(options: sendableOptions.value) + + switch result { + case .success(let routeResponse): + guard let navigationRoutes = try? await NavigationRoutes( + routeResponse: RouteResponse( + matching: routeResponse, + options: options, + credentials: .init(sendableSelf.value.configuration.nativeHandlersFactory.apiConfiguration) + ), + routeIndex: 0, + responseOrigin: origin + ) else { + throw DirectionsError.unableToRoute + } + return navigationRoutes + case .failure(let error): + throw error + } + } + } + + private func doRequest(options: DirectionsOptions) async -> (Result< + ResponseType, + DirectionsError + >, RouterOrigin) { + let directionsUri = Directions.url(forCalculating: options, credentials: configuration.credentials) + .removingSKU().absoluteString + let (result, origin) = await withCheckedContinuation { continuation in + let routeSignature = GetRouteSignature(reason: .newRoute, origin: .platformSDK, comment: "") + router.getRouteForDirectionsUri( + directionsUri, + options: GetRouteOptions(timeoutSeconds: nil), + caller: routeSignature + ) { ( + result: Expected, + origin: RouterOrigin + ) in + continuation.resume(returning: (result, origin)) + } + } + + return await ( + parseResponse( + userInfo: [ + .options: options, + .credentials: Credentials(configuration.nativeHandlersFactory.apiConfiguration), + ], + result: result + ), + origin + ) + } + + private func parseResponse( + userInfo: [CodingUserInfoKey: Any], + result: Expected + ) async -> Result { + guard let dataRef = result.value else { + return .failure(.noData) + } + + return parseResponse( + userInfo: userInfo, + result: dataRef.data, + error: result.error as? Error + ) + } + + private func parseResponse( + userInfo: [CodingUserInfoKey: Any], + result: Expected + ) async -> Result { + let json = result.value as String? + guard let data = json?.data(using: .utf8) else { + return .failure(.noData) + } + + return parseResponse( + userInfo: userInfo, + result: data, + error: result.error as? Error + ) + } + + private func parseResponse( + userInfo: [CodingUserInfoKey: Any], + result data: Data, + error: Error? + ) -> Result { + do { + let decoder = JSONDecoder() + decoder.userInfo = userInfo + + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = DirectionsError( + code: nil, + message: nil, + response: nil, + underlyingError: error + ) + return .failure(apiError) + } + + guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { + let apiError = DirectionsError( + code: disposition.code, + message: disposition.message, + response: nil, + underlyingError: error + ) + return .failure(apiError) + } + + let result = try decoder.decode(ResponseType.self, from: data) + return .success(result) + } catch { + let bailError = DirectionsError(code: nil, message: nil, response: nil, underlyingError: error) + return .failure(bailError) + } + } +} + +extension ProfileIdentifier { + var nativeProfile: RoutingProfile { + let mode: RoutingMode = switch self { + case .automobile: + .driving + case .automobileAvoidingTraffic: + .drivingTraffic + case .cycling: + .cycling + case .walking: + .walking + default: + .driving + } + return RoutingProfile(mode: mode, account: "mapbox") + } +} + +extension URL { + func removingSKU() -> URL { + var urlComponents = URLComponents(string: absoluteString)! + let filteredItems = urlComponents.queryItems?.filter { $0.name != "sku" } + urlComponents.queryItems = filteredItems + return urlComponents.url! + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Routing/NavigationRouteOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Routing/NavigationRouteOptions.swift new file mode 100644 index 000000000..7ceed37e2 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Routing/NavigationRouteOptions.swift @@ -0,0 +1,227 @@ +import _MapboxNavigationHelpers +import CoreLocation +import Foundation +import MapboxDirections + +/// A ``NavigationRouteOptions`` object specifies turn-by-turn-optimized criteria for results returned by the Mapbox +/// Directions API. +/// +/// ``NavigationRouteOptions`` is a subclass of `RouteOptions` that has been optimized for navigation. Pass an instance +/// of this class into the ``RoutingProvider/calculateRoutes(options:)-3d0sf`` method. +/// +/// This class implements the `NSCopying` protocol by round-tripping the object through `JSONEncoder` and `JSONDecoder`. +/// If you subclass ``NavigationRouteOptions``, make sure any properties you add are accounted for in `Decodable(from:)` +/// and `Encodable.encode(to:)`. If your subclass contains any customizations that cannot be represented in JSON, make +/// sure the subclass overrides `NSCopying.copy(with:)` to persist those customizations. +/// +/// ``NavigationRouteOptions`` is designed to be used with the ``MapboxRoutingProvider`` class for specifying routing +/// criteria. +open class NavigationRouteOptions: RouteOptions, OptimizedForNavigation, @unchecked Sendable { + /// Specifies the preferred distance measurement unit. + /// + /// Meters and feet will be used when the presented distances are small enough. See `DistanceFormatter` for more + /// information. + public var distanceUnit: LengthFormatter.Unit = Locale.current.usesMetricSystem ? .kilometer : .mile + + public convenience init( + waypoints: [Waypoint], + profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, + queryItems: [URLQueryItem]? = nil, + locale: Locale, + distanceUnit: LengthFormatter.Unit + ) { + self.init( + waypoints: waypoints, + profileIdentifier: profileIdentifier, + queryItems: queryItems + ) + self.locale = locale + self.distanceUnit = distanceUnit + } + + /// Initializes a navigation route options object for routes between the given waypoints and an optional profile + /// identifier optimized for navigation. + public required init( + waypoints: [Waypoint], + profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, + queryItems: [URLQueryItem]? = nil + ) { + super.init( + waypoints: waypoints.map { waypoint in + with(waypoint) { + $0.coordinateAccuracy = -1 + } + }, + profileIdentifier: profileIdentifier, + queryItems: queryItems + ) + includesAlternativeRoutes = true + attributeOptions = [.expectedTravelTime, .maximumSpeedLimit] + if profileIdentifier == .automobileAvoidingTraffic { + attributeOptions.update(with: .numericCongestionLevel) + } + includesExitRoundaboutManeuver = true + if profileIdentifier == .automobileAvoidingTraffic { + refreshingEnabled = true + } + + optimizeForNavigation() + } + + /// Initializes an equivalent `RouteOptions` object from a ``NavigationMatchOptions``. + /// + /// - SeeAlso: ``NavigationMatchOptions``. + public convenience init(navigationMatchOptions options: NavigationMatchOptions) { + self.init(waypoints: options.waypoints, profileIdentifier: options.profileIdentifier) + } + + /// Initializes a navigation route options object for routes between the given locations and an optional profile + /// identifier optimized for navigation. + public convenience init( + locations: [CLLocation], + profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, + queryItems: [URLQueryItem]? = nil + ) { + self.init( + waypoints: locations.map { Waypoint(location: $0) }, + profileIdentifier: profileIdentifier, + queryItems: queryItems + ) + } + + /// Initializes a route options object for routes between the given geographic coordinates and an optional profile + /// identifier optimized for navigation. + public convenience init( + coordinates: [CLLocationCoordinate2D], + profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, + queryItems: [URLQueryItem]? = nil + ) { + self.init( + waypoints: coordinates.map { Waypoint(coordinate: $0) }, + profileIdentifier: profileIdentifier, + queryItems: queryItems + ) + } + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + } +} + +/// A ``NavigationMatchOptions`` object specifies turn-by-turn-optimized criteria for results returned by the Mapbox Map +/// Matching API. +/// +/// `NavigationMatchOptions`` is a subclass of `MatchOptions` that has been optimized for navigation. Pass an instance +/// of this class into the `Directions.calculateRoutes(matching:completionHandler:).` method. +/// +/// - Note: it is very important you specify the `waypoints` for the route. Usually the only two values for this +/// `IndexSet` will be 0 and the length of the coordinates. Otherwise, all coordinates passed through will be considered +/// waypoints. +open class NavigationMatchOptions: MatchOptions, OptimizedForNavigation, @unchecked Sendable { + /// Specifies the preferred distance measurement unit. + /// + /// Meters and feet will be used when the presented distances are small enough. See `DistanceFormatter` for more + /// information. + public var distanceUnit: LengthFormatter.Unit = Locale.current.usesMetricSystem ? .kilometer : .mile + + public convenience init( + waypoints: [Waypoint], + profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, + queryItems: [URLQueryItem]? = nil, + distanceUnit: LengthFormatter.Unit + ) { + self.init( + waypoints: waypoints, + profileIdentifier: profileIdentifier, + queryItems: queryItems + ) + self.distanceUnit = distanceUnit + } + + /// Initializes a navigation route options object for routes between the given waypoints and an optional profile + /// identifier optimized for navigation. + /// + /// - Seealso: `RouteOptions`. + public required init( + waypoints: [Waypoint], + profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, + queryItems: [URLQueryItem]? = nil + ) { + super.init( + waypoints: waypoints.map { waypoint in + with(waypoint) { + $0.coordinateAccuracy = -1 + } + }, + profileIdentifier: profileIdentifier, + queryItems: queryItems + ) + attributeOptions = [.expectedTravelTime] + if profileIdentifier == .automobileAvoidingTraffic { + attributeOptions.update(with: .numericCongestionLevel) + } + if profileIdentifier == .automobile || profileIdentifier == .automobileAvoidingTraffic { + attributeOptions.insert(.maximumSpeedLimit) + } + + optimizeForNavigation() + } + + /// Initializes a navigation match options object for routes between the given locations and an optional profile + /// identifier optimized for navigation. + public convenience init( + locations: [CLLocation], + profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, + queryItems: [URLQueryItem]? = nil + ) { + self.init( + waypoints: locations.map { Waypoint(location: $0) }, + profileIdentifier: profileIdentifier, + queryItems: queryItems + ) + } + + /// Initializes a navigation match options object for routes between the given geographic coordinates and an + /// optional profile identifier optimized for navigation. + public convenience init( + coordinates: [CLLocationCoordinate2D], + profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, + queryItems: [URLQueryItem]? = nil + ) { + self.init( + waypoints: coordinates.map { Waypoint(coordinate: $0) }, + profileIdentifier: profileIdentifier, + queryItems: queryItems + ) + } + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + } +} + +protocol OptimizedForNavigation: AnyObject { + var includesSteps: Bool { get set } + var routeShapeResolution: RouteShapeResolution { get set } + var shapeFormat: RouteShapeFormat { get set } + var attributeOptions: AttributeOptions { get set } + var locale: Locale { get set } + var distanceMeasurementSystem: MeasurementSystem { get set } + var includesSpokenInstructions: Bool { get set } + var includesVisualInstructions: Bool { get set } + var distanceUnit: LengthFormatter.Unit { get } + + func optimizeForNavigation() +} + +extension OptimizedForNavigation { + func optimizeForNavigation() { + shapeFormat = .polyline6 + includesSteps = true + routeShapeResolution = .full + includesSpokenInstructions = true + locale = Locale.nationalizedCurrent + distanceMeasurementSystem = .init(distanceUnit) + includesVisualInstructions = true + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Routing/RoutingProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Routing/RoutingProvider.swift new file mode 100644 index 000000000..05914e1d1 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Routing/RoutingProvider.swift @@ -0,0 +1,45 @@ +import MapboxDirections +import MapboxNavigationNative + +/// Allows fetching ``NavigationRoutes`` by given parameters. +public protocol RoutingProvider: Sendable { + /// An asynchronous cancellable task for fetching a route. + typealias FetchTask = Task + + /// Creates a route by given `options`. + /// + /// This may be online or offline route, depending on the configuration and network availability. + func calculateRoutes(options: RouteOptions) -> FetchTask + + /// Creates a map matched route by given `options`. + /// + /// This may be online or offline route, depending on the configuration and network availability. + func calculateRoutes(options: MatchOptions) -> FetchTask +} + +/// Defines source of routing engine to be used for requests. +public enum RoutingProviderSource: Equatable, Sendable { + /// Fetch data online only. + /// + /// Such ``MapboxRoutingProvider`` is equivalent of using bare `Directions` wrapper. + case online + /// Use offline data only. + /// + /// In order for such ``MapboxRoutingProvider`` to function properly, proper navigation data should be available + /// offline. `.offline` routing provider will not be able to refresh routes. + case offline + /// Attempts to use ``RoutingProviderSource/online`` with fallback to ``RoutingProviderSource/offline``. + /// `.hybrid` routing provider will be able to refresh routes only using internet connection. + case hybrid + + var nativeSource: RouterType { + switch self { + case .online: + return .online + case .offline: + return .onboard + case .hybrid: + return .hybrid + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/SdkInfo.swift b/ios/Classes/Navigation/MapboxNavigationCore/SdkInfo.swift new file mode 100644 index 000000000..98b642614 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/SdkInfo.swift @@ -0,0 +1,23 @@ +import MapboxCommon_Private + +public struct SdkInfo: Sendable { + public static let navigationUX: Self = .init( + name: Bundle.resolvedNavigationSDKName, + version: Bundle.mapboxNavigationVersion, + packageName: "com.mapbox.navigationUX" + ) + + public static let navigationCore: Self = .init( + name: Bundle.navigationCoreName, + version: Bundle.mapboxNavigationVersion, + packageName: "com.mapbox.navigationCore" + ) + + public let name: String + public let version: String + public let packageName: String + + var native: SdkInformation { + .init(name: name, version: version, packageName: packageName) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/AlternativeRoutesDetectionConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/AlternativeRoutesDetectionConfig.swift new file mode 100644 index 000000000..ba8326079 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/AlternativeRoutesDetectionConfig.swift @@ -0,0 +1,103 @@ +import Foundation + +/// Options to configure fetching, detecting, and accepting ``AlternativeRoute``s during navigation. +public struct AlternativeRoutesDetectionConfig: Equatable, Sendable { + public struct AcceptionPolicy: OptionSet, Sendable { + public typealias RawValue = UInt + public var rawValue: UInt + + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + public static let unfiltered = AcceptionPolicy(rawValue: 1 << 0) + public static let fasterRoutes = AcceptionPolicy(rawValue: 1 << 1) + public static let shorterRoutes = AcceptionPolicy(rawValue: 1 << 2) + } + + /// Enables requesting for new alternative routes after passing a fork (intersection) where another alternative + /// route branches. The default value is `true`. + @available(*, deprecated, message: "This feature no longer has any effect.") + public var refreshesAfterPassingDeviation = true + + /// Enables periodic requests when there are no known alternative routes yet. The default value is + /// ``AlternativeRoutesDetectionConfig/RefreshOnEmpty/noPeriodicRefresh``. + @available( + *, + deprecated, + message: "This feature no longer has any effect other then setting the refresh interval. Use 'refreshIntervalSeconds' instead to configure the refresh interval directly." + ) + public var refreshesWhenNoAvailableAlternatives: RefreshOnEmpty = .noPeriodicRefresh { + didSet { + if let refreshIntervalSeconds = refreshesWhenNoAvailableAlternatives.refreshIntervalSeconds { + self.refreshIntervalSeconds = refreshIntervalSeconds + } + } + } + + /// Describes how periodic requests for ``AlternativeRoute``s should be made. + @available(*, deprecated, message: "This feature no longer has any effect.") + public enum RefreshOnEmpty: Equatable, Sendable { + /// Will not do periodic requests for alternatives. + case noPeriodicRefresh + /// Requests will be made with the given time interval. Using this option may result in increased traffic + /// consumption, but help detect alternative routes + /// which may appear during road conditions change during the trip. The default value is `5 minutes`. + case refreshesPeriodically(TimeInterval = AlternativeRoutesDetectionConfig.defaultRefreshIntervalSeconds) + } + + public var acceptionPolicy: AcceptionPolicy + + /// The refresh alternative routes interval. 5 minutes by default. Minimum 30 seconds. + public var refreshIntervalSeconds: TimeInterval + + /// Creates a new alternative routes detection configuration. + /// + /// - Parameters: + /// - refreshesAfterPassingDeviation: Enables requesting for new alternative routes after passing a fork + /// (intersection) where another alternative route branches. + /// The default value is `true`. + /// - refreshesWhenNoAvailableAlternatives: Enables periodic requests when there are no known alternative routes + /// yet. The default value is ``AlternativeRoutesDetectionConfig/RefreshOnEmpty/noPeriodicRefresh``. + /// - acceptionPolicy: The acceptance policy. + @available(*, deprecated, message: "Use 'init(acceptionPolicy:refreshIntervalSeconds:)' instead.") + public init( + refreshesAfterPassingDeviation: Bool = true, + refreshesWhenNoAvailableAlternatives: RefreshOnEmpty = .noPeriodicRefresh, + acceptionPolicy: AcceptionPolicy = .unfiltered + ) { + self.refreshesAfterPassingDeviation = refreshesAfterPassingDeviation + self.refreshesWhenNoAvailableAlternatives = refreshesWhenNoAvailableAlternatives + self.acceptionPolicy = acceptionPolicy + self.refreshIntervalSeconds = refreshesWhenNoAvailableAlternatives.refreshIntervalSeconds ?? Self + .defaultRefreshIntervalSeconds + } + + /// Creates a new alternative routes detection configuration. + /// + /// - Parameters: + /// - acceptionPolicy: The acceptance policy. + /// - refreshIntervalSeconds: The refresh alternative routes interval. 5 minutes by default. Minimum 30 + /// seconds. + public init( + acceptionPolicy: AcceptionPolicy = .unfiltered, + refreshIntervalSeconds: TimeInterval = Self.defaultRefreshIntervalSeconds + ) { + self.acceptionPolicy = acceptionPolicy + self.refreshIntervalSeconds = refreshIntervalSeconds + } + + public static let defaultRefreshIntervalSeconds: TimeInterval = 300 +} + +@available(*, deprecated) +extension AlternativeRoutesDetectionConfig.RefreshOnEmpty { + fileprivate var refreshIntervalSeconds: TimeInterval? { + switch self { + case .noPeriodicRefresh: + return nil + case .refreshesPeriodically(let value): + return value + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/BillingHandlerProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/BillingHandlerProvider.swift new file mode 100644 index 000000000..4bb51d464 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/BillingHandlerProvider.swift @@ -0,0 +1,16 @@ +import Foundation + +struct BillingHandlerProvider: Equatable { + static func == (lhs: BillingHandlerProvider, rhs: BillingHandlerProvider) -> Bool { + return lhs.object === rhs.object + } + + private var object: BillingHandler + func callAsFunction() -> BillingHandler { + return object + } + + init(_ object: BillingHandler) { + self.object = object + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/CoreConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/CoreConfig.swift new file mode 100644 index 000000000..f81cc3779 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/CoreConfig.swift @@ -0,0 +1,222 @@ +import CoreLocation +import Foundation +import MapboxCommon +import MapboxDirections + +/// Mutable Core SDK configuration. +public struct CoreConfig: Equatable { + /// Describes the context under which a manual switching between legs is happening. + public struct MultiLegAdvanceContext: Sendable, Equatable { + /// The leg index of a destination user has arrived to. + public let arrivedLegIndex: Int + } + + /// Allows to manually or automatically switch legs on a multileg route. + public typealias MultilegAdvanceMode = ApprovalModeAsync + + /// SDK Credentials. + public let credentials: NavigationCoreApiConfiguration + + /// Configures route request. + public var routeRequestConfig: RouteRequestConfig + + /// Routing Configuration. + public var routingConfig: RoutingConfig + + /// Custom metadata that can be used with events in the telemetry pipeline. + public let telemetryAppMetadata: TelemetryAppMetadata? + + /// Sources for location and route drive simulation. Defaults to ``LocationSource/live``. + public var locationSource: LocationSource + + /// Logging level for Mapbox SDKs. Defaults to `.warning`. + public var logLevel: MapboxCommon.LoggingLevel + + /// A Boolean value that indicates whether a copilot recording is enabled. Defaults to `false`. + public let copilotEnabled: Bool + + /// Configures default unit of measurement. + public var unitOfMeasurement: UnitOfMeasurement = .auto + + /// A `Locale` that is used for guidance instruction and other localization features. + public var locale: Locale = .nationalizedCurrent + + /// A Boolean value that indicates whether a background location tracking is enabled. Defaults to `true`. + public let disableBackgroundTrackingLocation: Bool + + /// A Boolean value that indicates whether a sensor data is utilized. Defaults to `false`. + public let utilizeSensorData: Bool + + /// Defines approximate navigator prediction between location ticks. + /// Due to discrete location updates, Navigator always operates data "in the past" so it has to make prediction + /// about user's current real position. This interval controls how far ahead Navigator will try to predict user + /// location. + public let navigatorPredictionInterval: TimeInterval? + + /// Congestion level configuration. + public var congestionConfig: CongestionRangesConfiguration + + /// Configuration for navigation history recording. + public let historyRecordingConfig: HistoryRecordingConfig? + + /// Predictive cache configuration. + public var predictiveCacheConfig: PredictiveCacheConfig? + + /// Electronic Horizon Configuration. + public var electronicHorizonConfig: ElectronicHorizonConfig? + + /// Electronic Horizon incidents configuration. + public let liveIncidentsConfig: IncidentsConfig? + + /// Multileg advancing mode. + public var multilegAdvancing: MultilegAdvanceMode + + /// Tiles version. + public let tilesVersion: String + + /// Options for configuring how map and navigation tiles are stored on the device. + public let tilestoreConfig: TileStoreConfiguration + + /// Configuration for Text-To-Speech engine used. + public var ttsConfig: TTSConfig + + /// Billing handler overriding for testing purposes. + var __customBillingHandler: BillingHandlerProvider? = nil + + /// Events manager overriding for testing purposes. + var __customEventsManager: EventsManagerProvider? = nil + + /// Routing provider overriding for testing purposes. + var __customRoutingProvider: CustomRoutingProvider? = nil + + /// Mutable Routing configuration. + public struct RouteRequestConfig: Equatable, Sendable { + /// A string specifying the primary mode of transportation for the routes. + /// `ProfileIdentifier.automobileAvoidingTraffic` is used by default. + public let profileIdentifier: ProfileIdentifier + + /// The route classes that the calculated routes will avoid. + public var roadClassesToAvoid: RoadClasses + + /// The route classes that the calculated routes will allow. + /// This property has no effect unless the profile identifier is set to `ProfileIdentifier.automobile` or + /// `ProfileIdentifier.automobileAvoidingTraffic`. + public var roadClassesToAllow: RoadClasses + + /// A Boolean value that indicates whether a returned route may require a point U-turn at an intermediate + /// waypoint. + /// + /// If the value of this property is `true`, a returned route may require an immediate U-turn at an + /// intermediate + /// waypoint. At an intermediate waypoint, if the value of this property is `false`, each returned route may + /// continue straight ahead or turn to either side but may not U-turn. This property has no effect if only two + /// waypoints are specified. + /// + /// Set this property to `true` if you expect the user to traverse each leg of the trip separately. For + /// example, it + /// would be quite easy for the user to effectively “U-turn” at a waypoint if the user first parks the car and + /// patronizes a restaurant there before embarking on the next leg of the trip. Set this property to `false` if + /// you + /// expect the user to proceed to the next waypoint immediately upon arrival. For example, if the user only + /// needs to + /// drop off a passenger or package at the waypoint before continuing, it would be inconvenient to perform a + /// U-turn + /// at that location. + /// The default value of this property is `false. + public var allowsUTurnAtWaypoint: Bool + + /// URL query items to be parsed and applied as configuration to the route request. + public var customQueryParameters: [URLQueryItem]? + + /// Initializes a new `CoreConfig` object. + /// - Parameters: + /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. + /// - roadClassesToAvoid: The route classes that the calculated routes will avoid. + /// - roadClassesToAllow: The route classes that the calculated routes will allow. + /// - allowsUTurnAtWaypoint: A Boolean value that indicates whether a returned route may require a point + /// - customQueryParameters: URL query items to be parsed and applied as configuration to the route request. + /// U-turn at an intermediate waypoint. + public init( + profileIdentifier: ProfileIdentifier = .automobileAvoidingTraffic, + roadClassesToAvoid: RoadClasses = [], + roadClassesToAllow: RoadClasses = [], + allowsUTurnAtWaypoint: Bool = false, + customQueryParameters: [URLQueryItem]? = nil + ) { + self.profileIdentifier = profileIdentifier + self.roadClassesToAvoid = roadClassesToAvoid + self.roadClassesToAllow = roadClassesToAllow + self.allowsUTurnAtWaypoint = allowsUTurnAtWaypoint + self.customQueryParameters = customQueryParameters + } + } + + /// Creates a new ``CoreConfig`` instance. + /// - Parameters: + /// - credentials: SDK Credentials. + /// - routeRequestConfig: Route requiest configuration + /// - routingConfig: Routing Configuration. + /// - telemetryAppMetadata: Custom metadata that can be used with events in the telemetry pipeline. + /// - logLevel: Logging level for Mapbox SDKs. + /// - isSimulationEnabled: A Boolean value that indicates whether a route simulation is enabled. + /// - copilotEnabled: A Boolean value that indicates whether a copilot recording is enabled. + /// - unitOfMeasurement: Configures default unit of measurement. + /// - locale: A `Locale` that is used for guidance instruction and other localization features. + /// - disableBackgroundTrackingLocation: Indicates if a background location tracking is enabled. + /// - utilizeSensorData: A Boolean value that indicates whether a sensor data is utilized. + /// - navigatorPredictionInterval: Defines approximate navigator prediction between location ticks. + /// - congestionConfig: Congestion level configuration. + /// - historyRecordingConfig: Configuration for navigation history recording. + /// - predictiveCacheConfig: Predictive cache configuration. + /// - electronicHorizonConfig: Electronic Horizon Configuration. + /// - liveIncidentsConfig: Electronic Horizon incidents configuration. + /// - multilegAdvancing: Multileg advancing mode. + /// - tilesVersion: Tiles version. + /// - tilestoreConfig: Options for configuring how map and navigation tiles are stored on the device. + /// - ttsConfig: Configuration for Text-To-Speech engine used. + public init( + credentials: NavigationCoreApiConfiguration = .init(), + routeRequestConfig: RouteRequestConfig = .init(), + routingConfig: RoutingConfig = .init(), + telemetryAppMetadata: TelemetryAppMetadata? = nil, + logLevel: MapboxCommon.LoggingLevel = .warning, + locationSource: LocationSource = .live, + copilotEnabled: Bool = false, + unitOfMeasurement: UnitOfMeasurement = .auto, + locale: Locale = .nationalizedCurrent, + disableBackgroundTrackingLocation: Bool = true, + utilizeSensorData: Bool = false, + navigatorPredictionInterval: TimeInterval? = nil, + congestionConfig: CongestionRangesConfiguration = .default, + historyRecordingConfig: HistoryRecordingConfig? = nil, + predictiveCacheConfig: PredictiveCacheConfig? = PredictiveCacheConfig(), + electronicHorizonConfig: ElectronicHorizonConfig? = nil, + liveIncidentsConfig: IncidentsConfig? = nil, + multilegAdvancing: MultilegAdvanceMode = .automatically, + tilesVersion: String = "", + tilestoreConfig: TileStoreConfiguration = .default, + ttsConfig: TTSConfig = .default + ) { + self.credentials = credentials + self.routeRequestConfig = routeRequestConfig + self.telemetryAppMetadata = telemetryAppMetadata + self.logLevel = logLevel + self.locationSource = locationSource + self.copilotEnabled = copilotEnabled + self.unitOfMeasurement = unitOfMeasurement + self.locale = locale + self.disableBackgroundTrackingLocation = disableBackgroundTrackingLocation + self.utilizeSensorData = utilizeSensorData + self.navigatorPredictionInterval = navigatorPredictionInterval + self.congestionConfig = congestionConfig + self.historyRecordingConfig = historyRecordingConfig + self.predictiveCacheConfig = predictiveCacheConfig + self.electronicHorizonConfig = electronicHorizonConfig + self.liveIncidentsConfig = liveIncidentsConfig + self.multilegAdvancing = multilegAdvancing + self.routingConfig = routingConfig + self.tilesVersion = tilesVersion + self.tilestoreConfig = tilestoreConfig + self.ttsConfig = ttsConfig + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/UnitOfMeasurement.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/UnitOfMeasurement.swift new file mode 100644 index 000000000..f26bd6923 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/UnitOfMeasurement.swift @@ -0,0 +1,11 @@ +import Foundation + +/// Holds available types of measurement units. +public enum UnitOfMeasurement: Equatable, Sendable { + /// Allows SDK to pick proper units. + case auto + /// Selects imperial units as default. + case imperial + /// Selects metric units as default. + case metric +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/CustomRoutingProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/CustomRoutingProvider.swift new file mode 100644 index 000000000..f85a569bf --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/CustomRoutingProvider.swift @@ -0,0 +1,16 @@ +import Foundation + +struct CustomRoutingProvider: Equatable { + static func == (lhs: CustomRoutingProvider, rhs: CustomRoutingProvider) -> Bool { + lhs.object === rhs.object + } + + private var object: RoutingProvider & AnyObject + func callAsFunction() -> RoutingProvider { + return object + } + + init(_ object: RoutingProvider & AnyObject) { + self.object = object + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/EventsManagerProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/EventsManagerProvider.swift new file mode 100644 index 000000000..783865181 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/EventsManagerProvider.swift @@ -0,0 +1,16 @@ +import Foundation + +struct EventsManagerProvider: Equatable { + static func == (lhs: EventsManagerProvider, rhs: EventsManagerProvider) -> Bool { + return lhs.object === rhs.object + } + + private var object: NavigationEventsManager + func callAsFunction() -> NavigationEventsManager { + return object + } + + init(_ object: NavigationEventsManager) { + self.object = object + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/FasterRouteDetectionConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/FasterRouteDetectionConfig.swift new file mode 100644 index 000000000..34148d485 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/FasterRouteDetectionConfig.swift @@ -0,0 +1,44 @@ +import CoreLocation +import Foundation + +/// Options to configure fetching, detecting, and accepting a faster route during active guidance. +public struct FasterRouteDetectionConfig: Equatable { + public static func == (lhs: FasterRouteDetectionConfig, rhs: FasterRouteDetectionConfig) -> Bool { + guard lhs.fasterRouteApproval == rhs.fasterRouteApproval, + lhs.proactiveReroutingInterval == rhs.proactiveReroutingInterval, + lhs.minimumRouteDurationRemaining == rhs.minimumRouteDurationRemaining, + lhs.minimumManeuverOffset == rhs.minimumManeuverOffset + else { + return false + } + + switch (lhs.customFasterRouteProvider, rhs.customFasterRouteProvider) { + case (.none, .none), (.some(_), .some(_)): + return true + default: + return false + } + } + + public typealias FasterRouteApproval = ApprovalModeAsync<(CLLocation, NavigationRoute)> + + public var fasterRouteApproval: FasterRouteApproval + public var proactiveReroutingInterval: TimeInterval + public var minimumRouteDurationRemaining: TimeInterval + public var minimumManeuverOffset: TimeInterval + public var customFasterRouteProvider: (any FasterRouteProvider)? + + public init( + fasterRouteApproval: FasterRouteApproval = .automatically, + proactiveReroutingInterval: TimeInterval = 120, + minimumRouteDurationRemaining: TimeInterval = 600, + minimumManeuverOffset: TimeInterval = 70, + customFasterRouteProvider: (any FasterRouteProvider)? = nil + ) { + self.fasterRouteApproval = fasterRouteApproval + self.proactiveReroutingInterval = proactiveReroutingInterval + self.minimumRouteDurationRemaining = minimumRouteDurationRemaining + self.minimumManeuverOffset = minimumManeuverOffset + self.customFasterRouteProvider = customFasterRouteProvider + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/HistoryRecordingConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/HistoryRecordingConfig.swift new file mode 100644 index 000000000..3f579c778 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/HistoryRecordingConfig.swift @@ -0,0 +1,16 @@ +import Foundation + +public struct HistoryRecordingConfig: Equatable, Sendable { + public static let defaultFolderName = "historyRecordings" + + public var historyDirectoryURL: URL + + public init( + historyDirectoryURL: URL = FileManager.default.urls( + for: .documentDirectory, + in: .userDomainMask + )[0].appendingPathComponent(defaultFolderName) + ) { + self.historyDirectoryURL = historyDirectoryURL + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/IncidentsConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/IncidentsConfig.swift new file mode 100644 index 000000000..1f1d4a0bc --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/IncidentsConfig.swift @@ -0,0 +1,26 @@ +import Foundation + +/// Configures how Electronic Horizon supports live incidents on a most probable path. +/// +/// To enable live incidents ``IncidentsConfig`` should be provided to ``CoreConfig/liveIncidentsConfig`` before +/// starting navigation. +public struct IncidentsConfig: Equatable, Sendable { + /// Incidents provider graph name. + /// + /// If empty - incidents will be disabled. + public var graph: String + + /// LTS incidents service API url. + /// + /// If `nil` is supplied will use a default url. + public var apiURL: URL? + + /// Creates new ``IncidentsConfig``. + /// - Parameters: + /// - graph: Incidents provider graph name. + /// - apiURL: LTS incidents service API url. + public init(graph: String, apiURL: URL?) { + self.graph = graph + self.apiURL = apiURL + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/NavigationCoreApiConfiguration.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/NavigationCoreApiConfiguration.swift new file mode 100644 index 000000000..467eba29e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/NavigationCoreApiConfiguration.swift @@ -0,0 +1,41 @@ +import MapboxDirections + +/// Allows to configure access token and endpoint for separate SDK requests separately for directions, maps, and speech +/// requests. +public struct NavigationCoreApiConfiguration: Equatable, Sendable { + /// The configuration used to make directions-related requests. + public let navigation: ApiConfiguration + /// The configuration used to make map-loading requests. + public let map: ApiConfiguration + /// The configuration used to make speech-related requests. + public let speech: ApiConfiguration + + /// Initializes ``NavigationCoreApiConfiguration`` instance. + /// - Parameters: + /// - navigation: The configuration used to make directions-related requests. + /// - map: The configuration used to make map-loading requests. + /// - speech: The configuration used to make speech-related requests. + public init( + navigation: ApiConfiguration = .default, + map: ApiConfiguration = .default, + speech: ApiConfiguration = .default + ) { + self.navigation = navigation + self.map = map + self.speech = speech + } +} + +extension NavigationCoreApiConfiguration { + /// Initializes ``NavigationCoreApiConfiguration`` instance. + /// - Parameter accessToken: A Mapbox [access token](https://www.mapbox.com/help/define-access-token/) used to + /// authorize Mapbox API requests. + public init(accessToken: String) { + let configuration = ApiConfiguration(accessToken: accessToken) + self.init( + navigation: configuration, + map: configuration, + speech: configuration + ) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/RerouteConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/RerouteConfig.swift new file mode 100644 index 000000000..370d69af3 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/RerouteConfig.swift @@ -0,0 +1,26 @@ +import Foundation +import MapboxDirections + +/// Configures the rerouting behavior. +public struct RerouteConfig: Equatable { + public typealias OptionsCustomization = EquatableClosure + + /// Optional customization callback triggered on reroute attempts. + /// + /// Provide this callback if you need to modify route request options, done during building a reroute. This will + /// not affect initial route requests. + public var optionsCustomization: OptionsCustomization? + /// Enables or disables rerouting mechanism. + /// + /// Disabling rerouting will result in the route remaining unchanged even if the user wanders off of it. + /// Reroute detecting is enabled by default. + public var detectsReroute: Bool + + public init( + detectsReroute: Bool = true, + optionsCustomization: OptionsCustomization? = nil + ) { + self.detectsReroute = detectsReroute + self.optionsCustomization = optionsCustomization + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/RoutingConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/RoutingConfig.swift new file mode 100644 index 000000000..e9335c340 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/RoutingConfig.swift @@ -0,0 +1,87 @@ +import Foundation + +/// Routing Configuration. +public struct RoutingConfig: Equatable { + /// Options to configure fetching, detecting, and accepting ``AlternativeRoute``s during navigation. + /// + /// Use `nil` value to disable the mechanism + public var alternativeRoutesDetectionConfig: AlternativeRoutesDetectionConfig? + + /// Options to configure fetching, detecting, and accepting a faster route during active guidance. + /// + /// Use `nil` value to disable the mechanism. + public var fasterRouteDetectionConfig: FasterRouteDetectionConfig? + + /// Configures the rerouting behavior. + public var rerouteConfig: RerouteConfig + + /// A radius around the current user position in which the API will avoid returning any significant maneuvers when + /// rerouting or suggesting alternative routes. + /// Provided `TimeInterval` value will be converted to meters using current speed. Default value is `8` seconds. + public var initialManeuverAvoidanceRadius: TimeInterval + + /// A time interval in which time-dependent properties of the ``RouteLeg``s of the resulting `Route`s will be + /// refreshed. + /// + /// This property is ignored unless ``profileIdentifier`` is `ProfileIdentifier.automobileAvoidingTraffic`. + /// Use `nil` value to disable the mechanism. + public var routeRefreshPeriod: TimeInterval? + + /// Type of routing to be used by various SDK objects when providing route calculations. Use this value to configure + /// online vs. offline data usage for routing. + /// + /// Default value is ``RoutingProviderSource/hybrid`` + public var routingProviderSource: RoutingProviderSource + + /// Enables automatic switching to online version of the current route when possible. + /// + /// Indicates if ``NavigationController`` will attempt to detect if thr current route was build offline and if there + /// is an online route with the same path is available to automatically switch to it. Using online route is + /// beneficial due to available live data like traffic congestion, incidents, etc. Check is not performed instantly + /// and it is not guaranteed to receive an online version at any given period of time. + /// + /// Enabled by default. + public var prefersOnlineRoute: Bool + + @available( + *, + deprecated, + message: "Use 'init(alternativeRoutesDetectionConfig:fasterRouteDetectionConfig:rerouteConfig:initialManeuverAvoidanceRadius:routeRefreshPeriod:routingProviderSource:prefersOnlineRoute:)' instead." + ) + public init( + alternativeRoutesDetectionSettings: AlternativeRoutesDetectionConfig? = .init(), + fasterRouteDetectionSettings: FasterRouteDetectionConfig? = .init(), + rerouteSettings: RerouteConfig = .init(), + initialManeuverAvoidanceRadius: TimeInterval = 8, + routeRefreshPeriod: TimeInterval? = 120, + routingProviderSource: RoutingProviderSource = .hybrid, + prefersOnlineRoute: Bool = true, + detectsReroute: Bool = true + ) { + self.alternativeRoutesDetectionConfig = alternativeRoutesDetectionSettings + self.fasterRouteDetectionConfig = fasterRouteDetectionSettings + self.rerouteConfig = rerouteSettings + self.initialManeuverAvoidanceRadius = initialManeuverAvoidanceRadius + self.routeRefreshPeriod = routeRefreshPeriod + self.routingProviderSource = routingProviderSource + self.prefersOnlineRoute = prefersOnlineRoute + } + + public init( + alternativeRoutesDetectionConfig: AlternativeRoutesDetectionConfig? = .init(), + fasterRouteDetectionConfig: FasterRouteDetectionConfig? = .init(), + rerouteConfig: RerouteConfig = .init(), + initialManeuverAvoidanceRadius: TimeInterval = 8, + routeRefreshPeriod: TimeInterval? = 120, + routingProviderSource: RoutingProviderSource = .hybrid, + prefersOnlineRoute: Bool = true + ) { + self.alternativeRoutesDetectionConfig = alternativeRoutesDetectionConfig + self.fasterRouteDetectionConfig = fasterRouteDetectionConfig + self.rerouteConfig = rerouteConfig + self.initialManeuverAvoidanceRadius = initialManeuverAvoidanceRadius + self.routeRefreshPeriod = routeRefreshPeriod + self.routingProviderSource = routingProviderSource + self.prefersOnlineRoute = prefersOnlineRoute + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/SettingsWrappers.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/SettingsWrappers.swift new file mode 100644 index 000000000..a27226461 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/SettingsWrappers.swift @@ -0,0 +1,40 @@ +import Foundation + +public enum ApprovalMode: Equatable, Sendable { + case automatically + case manually +} + +public enum ApprovalModeAsync: Equatable, Sendable { + public static func == (lhs: ApprovalModeAsync, rhs: ApprovalModeAsync) -> Bool { + switch (lhs, rhs) { + case (.automatically, .automatically), + (.manually(_), .manually(_)): + return true + default: + return false + } + } + + public typealias ApprovalCheck = @Sendable (Context) async -> Bool + + case automatically + case manually(ApprovalCheck) +} + +public struct EquatableClosure: Equatable { + public static func == (lhs: EquatableClosure, rhs: EquatableClosure) -> Bool { + return (lhs.closure != nil) == (rhs.closure != nil) + } + + public typealias Closure = (Input) -> Output + private var closure: Closure? + + func callAsFunction(_ input: Input) -> Output? { + return closure?(input) + } + + public init(_ closure: Closure? = nil) { + self.closure = closure + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/StatusUpdatingSettings.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/StatusUpdatingSettings.swift new file mode 100644 index 000000000..9fb3bb27c --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/StatusUpdatingSettings.swift @@ -0,0 +1,22 @@ +import Foundation + +/// Configures Navigator status polling. +public struct StatusUpdatingSettings { + /// If new location is not provided during ``updatingPatience`` - status will be polled unconditionally. + /// + /// If `nil` - default value will be used. + public var updatingPatience: TimeInterval? + /// Interval of unconditional status polling. + /// + /// If `nil` - default value will be used. + public var updatingInterval: TimeInterval? + + /// Creates new ``StatusUpdatingSettings``. + /// - Parameters: + /// - updatingPatience: The patience time before unconditional status polling. + /// - updatingInterval: The unconditional polling interval. + public init(updatingPatience: TimeInterval? = nil, updatingInterval: TimeInterval? = nil) { + self.updatingPatience = updatingPatience + self.updatingInterval = updatingInterval + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/TTSConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/TTSConfig.swift new file mode 100644 index 000000000..16b80fcab --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/TTSConfig.swift @@ -0,0 +1,19 @@ +import Foundation + +/// Text-To-Speech configuration. +public enum TTSConfig: Equatable, Sendable { + public static func == (lhs: TTSConfig, rhs: TTSConfig) -> Bool { + switch (lhs, rhs) { + case (.default, .default), + (.localOnly, .localOnly), + (.custom, .custom): + return true + default: + return false + } + } + + case `default` + case localOnly + case custom(speechSynthesizer: SpeechSynthesizing) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/TelemetryAppMetadata.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/TelemetryAppMetadata.swift new file mode 100644 index 000000000..6690dff96 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/TelemetryAppMetadata.swift @@ -0,0 +1,48 @@ +import CoreLocation +import Foundation + +/// Custom metadata that can be used with events in the telemetry pipeline. +public struct TelemetryAppMetadata: Equatable, Sendable { + /// Name of the application. + public let name: String + /// Version of the application. + public let version: String + /// User ID relevant for the application context. + public var userId: String? + /// Session ID relevant for the application context. + public var sessionId: String? + + /// nitializes a new `TelemetryAppMetadata` object. + /// - Parameters: + /// - name: Name of the application. + /// - version: Version of the application. + /// - userId: User ID relevant for the application context. + /// - sessionId: Session ID relevant for the application context. + public init( + name: String, + version: String, + userId: String?, + sessionId: String? + ) { + self.name = name + self.version = version + self.userId = userId + self.sessionId = sessionId + } +} + +extension TelemetryAppMetadata { + var configuration: [String: String?] { + var dictionary: [String: String?] = [ + "name": name, + "version": version, + ] + if let userId, !userId.isEmpty { + dictionary["userId"] = userId + } + if let sessionId, !sessionId.isEmpty { + dictionary["sessionId"] = sessionId + } + return dictionary + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/TileStoreConfiguration.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/TileStoreConfiguration.swift new file mode 100644 index 000000000..5736f6877 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Settings/TileStoreConfiguration.swift @@ -0,0 +1,65 @@ +import Foundation +import MapboxCommon + +/// Options for configuring how map and navigation tiles are stored on the device. +/// +/// This struct encapsulates logic for handling ``TileStoreConfiguration/Location/default`` and +/// ``TileStoreConfiguration/Location/custom(_:)`` paths as well as providing corresponding `TileStore`s. +/// It also covers differences between tile storages for Map and Navigation data. Tupically, you won't need to configure +/// these and rely on defaults, unless you provide pre-downloaded data withing your app in which case you'll need +/// ``TileStoreConfiguration/Location/custom(_:)`` path to point to your data. +public struct TileStoreConfiguration: Equatable, Sendable { + /// Describes filesystem location for tile storage folder + public enum Location: Equatable, Sendable { + /// Encapsulated default location. + /// + /// ``tileStoreURL`` for this case will return `nil`. + case `default` + /// User-provided path to tile storage folder. + case custom(URL) + + /// Corresponding URL path. + /// + /// ``TileStoreConfiguration/Location/default`` location is interpreted as `nil`. + public var tileStoreURL: URL? { + switch self { + case .default: + return nil + case .custom(let url): + return url + } + } + + /// A `TileStore` instance, configured for current location. + public var tileStore: TileStore { + switch self { + case .default: + return TileStore.__create() + case .custom(let url): + return TileStore.__create(forPath: url.path) + } + } + } + + /// Location of Navigator tiles data. + public let navigatorLocation: Location + + /// Location of Map tiles data. + public let mapLocation: Location? + + /// Tile data will be stored at default SDK location. + public static var `default`: Self { + .init(navigatorLocation: .default, mapLocation: .default) + } + + /// Custom path to a folder, where tiles data will be stored. + public static func custom(_ url: URL) -> Self { + .init(navigatorLocation: .custom(url), mapLocation: .custom(url)) + } + + /// Option to configure Map and Navigation tiles to be stored separately. You should not use this option unless you + /// know what you are doing. + public static func isolated(navigationLocation: Location, mapLocation: Location?) -> Self { + .init(navigatorLocation: navigationLocation, mapLocation: mapLocation) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/ConnectivityTypeProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/ConnectivityTypeProvider.swift new file mode 100644 index 000000000..37eb4b31e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/ConnectivityTypeProvider.swift @@ -0,0 +1,75 @@ +import _MapboxNavigationHelpers +import Foundation +import Network + +protocol ConnectivityTypeProvider { + var connectivityType: String { get } +} + +protocol NetworkMonitor: AnyObject, Sendable { + func start(queue: DispatchQueue) + var pathUpdateHandler: (@Sendable (_ newPath: NWPath) -> Void)? { get set } +} + +protocol NetworkPath { + var status: NWPath.Status { get } + func usesInterfaceType(_ type: NWInterface.InterfaceType) -> Bool +} + +extension NWPathMonitor: NetworkMonitor {} +extension NWPath: NetworkPath {} +extension NWPathMonitor: @unchecked Sendable {} + +final class MonitorConnectivityTypeProvider: ConnectivityTypeProvider, Sendable { + private let monitor: NetworkMonitor + private let monitorConnectionType = UnfairLocked(nil) + private let queue = DispatchQueue.global(qos: .utility) + + private static let connectionTypes: [NWInterface.InterfaceType] = [.cellular, .wifi, .wiredEthernet] + + var connectivityType: String { + monitorConnectionType.read()?.connectionType ?? "No Connection" + } + + init(monitor: NetworkMonitor = NWPathMonitor()) { + self.monitor = monitor + + configureMonitor() + } + + func handleChange(to path: NetworkPath) { + let newMonitorConnectionType: NWInterface.InterfaceType? + if path.status == .satisfied { + let connectionTypes = MonitorConnectivityTypeProvider.connectionTypes + newMonitorConnectionType = connectionTypes.first(where: path.usesInterfaceType) ?? .other + } else { + newMonitorConnectionType = nil + } + monitorConnectionType.update(newMonitorConnectionType) + } + + private func configureMonitor() { + monitor.pathUpdateHandler = { [weak self] path in + self?.handleChange(to: path) + } + monitor.start(queue: queue) + } +} + +extension NWInterface.InterfaceType { + fileprivate var connectionType: String { + switch self { + case .cellular: + return "Cellular" + case .wifi: + return "WiFi" + case .wiredEthernet: + return "Wired" + case .loopback, .other: + return "Unknown" + @unknown default: + Log.warning("Unexpected NWInterface.InterfaceType type", category: .settings) + return "Unexpected" + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventAppState.swift b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventAppState.swift new file mode 100644 index 000000000..23f238bf3 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventAppState.swift @@ -0,0 +1,153 @@ +import _MapboxNavigationHelpers +import UIKit + +final class EventAppState: Sendable { + struct Environment { + let date: @Sendable () -> Date + let applicationState: @Sendable () -> UIApplication.State + let screenOrientation: @Sendable () -> UIDeviceOrientation + let deviceOrientation: @Sendable () -> UIDeviceOrientation + + static var live: Self { + .init( + date: { Date() }, + applicationState: { + onMainQueueSync { + UIApplication.shared.applicationState + } + }, + screenOrientation: { + onMainQueueSync { + UIDevice.current.screenOrientation + } + }, + deviceOrientation: { + onMainQueueSync { + UIDevice.current.orientation + } + } + ) + } + } + + private let environment: Environment + private let innerState: UnfairLocked + private let sessionStarted: Date + + private struct State { + var timeSpentInPortrait: TimeInterval = 0 + var lastOrientation: UIDeviceOrientation + var lastTimeOrientationChanged: Date + + var timeInBackground: TimeInterval = 0 + var lastTimeEnteredBackground: Date? + } + + var percentTimeInForeground: Int { + let state = innerState.read() + let currentDate = environment.date() + var totalTimeInBackground = state.timeInBackground + if let lastTimeEnteredBackground = state.lastTimeEnteredBackground { + totalTimeInBackground += currentDate.timeIntervalSince(lastTimeEnteredBackground) + } + + let totalTime = currentDate.timeIntervalSince(sessionStarted) + return totalTime > 0 ? Int(100 * (totalTime - totalTimeInBackground) / totalTime) : 100 + } + + var percentTimeInPortrait: Int { + let state = innerState.read() + let currentDate = environment.date() + var totalTimeInPortrait = state.timeSpentInPortrait + if state.lastOrientation.isPortrait { + totalTimeInPortrait += currentDate.timeIntervalSince(state.lastTimeOrientationChanged) + } + + let totalTime = currentDate.timeIntervalSince(sessionStarted) + return totalTime > 0 ? Int(100 * totalTimeInPortrait / totalTime) : 100 + } + + @MainActor + init(environment: Environment = .live) { + self.environment = environment + + let date = environment.date() + self.sessionStarted = date + let lastOrientation = environment.screenOrientation() + let lastTimeOrientationChanged = date + let lastTimeEnteredBackground: Date? = environment.applicationState() == .background ? date : nil + let innerState = State( + lastOrientation: lastOrientation, + lastTimeOrientationChanged: lastTimeOrientationChanged, + lastTimeEnteredBackground: lastTimeEnteredBackground + ) + self.innerState = .init(innerState) + + subscribeNotifications() + } + + // MARK: - State Management + + private func subscribeNotifications() { + NotificationCenter.default.addObserver( + self, + selector: #selector(willEnterForegroundState), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(didEnterBackgroundState), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(didChangeOrientation), + name: UIDevice.orientationDidChangeNotification, + object: nil + ) + } + + @objc + private func didChangeOrientation() { + handleOrientationChange() + } + + @objc + private func didEnterBackgroundState() { + let date = environment.date() + innerState.mutate { + $0.lastTimeEnteredBackground = date + } + } + + @objc + private func willEnterForegroundState() { + let state = innerState.read() + guard let dateEnteredBackground = state.lastTimeEnteredBackground else { return } + + let timeDelta = environment.date().timeIntervalSince(dateEnteredBackground) + innerState.mutate { + $0.timeInBackground += timeDelta + $0.lastTimeEnteredBackground = nil + } + } + + private func handleOrientationChange() { + let state = innerState.read() + let orientation = environment.deviceOrientation() + guard orientation.isValidInterfaceOrientation else { return } + guard state.lastOrientation.isPortrait != orientation.isPortrait || + state.lastOrientation.isLandscape != orientation.isLandscape else { return } + + let currentDate = environment.date() + let timePortraitDelta = orientation.isLandscape ? currentDate + .timeIntervalSince(state.lastTimeOrientationChanged) : 0 + innerState.mutate { + $0.timeSpentInPortrait += timePortraitDelta + $0.lastTimeOrientationChanged = currentDate + $0.lastOrientation = orientation + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventsMetadataProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventsMetadataProvider.swift new file mode 100644 index 000000000..3cbc80b56 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventsMetadataProvider.swift @@ -0,0 +1,176 @@ +import _MapboxNavigationHelpers +import AVFoundation +import MapboxNavigationNative +import UIKit + +protocol AudioSessionInfoProvider { + var outputVolume: Float { get } + var telemetryAudioType: AudioType { get } +} + +final class EventsMetadataProvider: EventsMetadataInterface, Sendable { + let _userInfo: UnfairLocked<[String: String?]?> + var userInfo: [String: String?]? { + get { + _userInfo.read() + } + set { + _userInfo.update(newValue) + } + } + + private let screen: UIScreen + private let audioSessionInfoProvider: UnfairLocked + private let device: UIDevice + private let connectivityTypeProvider: UnfairLocked + private let appState: EventAppState + + @MainActor + init( + appState: EventAppState, + screen: UIScreen, + audioSessionInfoProvider: AudioSessionInfoProvider = AVAudioSession.sharedInstance(), + device: UIDevice, + connectivityTypeProvider: ConnectivityTypeProvider = MonitorConnectivityTypeProvider() + ) { + self.appState = appState + self.screen = screen + self.audioSessionInfoProvider = .init(audioSessionInfoProvider) + self.device = device + self.connectivityTypeProvider = .init(connectivityTypeProvider) + self._userInfo = .init(nil) + + device.isBatteryMonitoringEnabled = true + + self.batteryLevel = .init(Self.currentBatteryLevel(with: device)) + self.batteryPluggedIn = .init(Self.currentBatteryPluggedIn(with: device)) + self.screenBrightness = .init(Int(screen.brightness * 100)) + + let notificationCenter = NotificationCenter.default + notificationCenter.addObserver( + self, + selector: #selector(batteryLevelDidChange), + name: UIDevice.batteryLevelDidChangeNotification, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(batteryStateDidChange), + name: UIDevice.batteryStateDidChangeNotification, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(brightnessDidChange), + name: UIScreen.brightnessDidChangeNotification, + object: nil + ) + } + + private var appMetadata: AppMetadata? { + guard let userInfo, + let appName = userInfo["name"] as? String, + let appVersion = userInfo["version"] as? String else { return nil } + + return AppMetadata( + name: appName, + version: appVersion, + userId: userInfo["userId"] as? String, + sessionId: userInfo["sessionId"] as? String + ) + } + + private let screenBrightness: UnfairLocked + private var volumeLevel: Int { Int(audioSessionInfoProvider.read().outputVolume * 100) } + private var audioType: AudioType { audioSessionInfoProvider.read().telemetryAudioType } + + private let batteryPluggedIn: UnfairLocked + private let batteryLevel: UnfairLocked + private var connectivity: String { connectivityTypeProvider.read().connectivityType } + + func provideEventsMetadata() -> EventsMetadata { + return EventsMetadata( + volumeLevel: volumeLevel as NSNumber, + audioType: audioType.rawValue as NSNumber, + screenBrightness: screenBrightness.read() as NSNumber, + percentTimeInForeground: appState.percentTimeInForeground as NSNumber, + percentTimeInPortrait: appState.percentTimeInPortrait as NSNumber, + batteryPluggedIn: batteryPluggedIn.read() as NSNumber, + batteryLevel: batteryLevel.read() as NSNumber?, + connectivity: connectivity, + appMetadata: appMetadata + ) + } + + @objc + private func batteryLevelDidChange() { + let newValue = Self.currentBatteryLevel(with: device) + batteryLevel.update(newValue) + } + + private static func currentBatteryLevel(with device: UIDevice) -> Int? { + device.batteryLevel >= 0 ? Int(device.batteryLevel * 100) : nil + } + + @objc + private func batteryStateDidChange() { + let newValue = Self.currentBatteryPluggedIn(with: device) + batteryPluggedIn.update(newValue) + } + + private static let chargingStates: [UIDevice.BatteryState] = [.charging, .full] + + private static func currentBatteryPluggedIn(with device: UIDevice) -> Bool { + chargingStates.contains(device.batteryState) + } + + @objc + private func brightnessDidChange() { + screenBrightness.update(Int(screen.brightness * 100)) + } +} + +extension AVAudioSession: AudioSessionInfoProvider { + var telemetryAudioType: AudioType { + if currentRoute.outputs + .contains(where: { [.bluetoothA2DP, .bluetoothHFP, .bluetoothLE].contains($0.portType) }) + { + return .bluetooth + } + if currentRoute.outputs + .contains(where: { [.headphones, .airPlay, .HDMI, .lineOut, .carAudio, .usbAudio].contains($0.portType) }) + { + return .headphones + } + if currentRoute.outputs.contains(where: { [.builtInSpeaker, .builtInReceiver].contains($0.portType) }) { + return .speaker + } + return .unknown + } +} + +private final class BlockingOperation: @unchecked Sendable { + private var result: Result? + + func run(_ operation: @Sendable @escaping () async -> T) -> T? { + Task { + let task = Task(operation: operation) + self.result = await task.result + } + DispatchQueue.global().sync { + while result == nil { + RunLoop.current.run(mode: .default, before: .distantFuture) + } + } + switch result { + case .success(let value): + return value + case .none: + assertionFailure("Running blocking operation did not receive a value.") + return nil + } + } +} + +extension EventsMetadata: @unchecked Sendable {} +extension ScreenshotFormat: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationNativeEventsManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationNativeEventsManager.swift new file mode 100644 index 000000000..5aead1a6d --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationNativeEventsManager.swift @@ -0,0 +1,174 @@ +import _MapboxNavigationHelpers +import CoreLocation +import Foundation +import MapboxCommon +import MapboxNavigationNative_Private +import UIKit + +final class NavigationNativeEventsManager: NavigationTelemetryManager, Sendable { + private let eventsMetadataProvider: EventsMetadataProvider + private let telemetry: UnfairLocked + + private let _userInfo: UnfairLocked<[String: String?]?> + var userInfo: [String: String?]? { + get { + _userInfo.read() + } + set { + _userInfo.update(newValue) + eventsMetadataProvider.userInfo = newValue + } + } + + required init(eventsMetadataProvider: EventsMetadataProvider, telemetry: Telemetry) { + self.eventsMetadataProvider = eventsMetadataProvider + self.telemetry = .init(telemetry) + self._userInfo = .init(nil) + } + + func createFeedback(screenshotOption: FeedbackScreenshotOption) async -> FeedbackEvent? { + let userFeedbackHandle = telemetry.read().startBuildUserFeedbackMetadata() + let screenshot = await createScreenshot(screenshotOption: screenshotOption) + let feedbackMetadata = FeedbackMetadata(userFeedbackHandle: userFeedbackHandle, screenshot: screenshot) + return FeedbackEvent(metadata: feedbackMetadata) + } + + func sendActiveNavigationFeedback( + _ feedback: FeedbackEvent, + type: ActiveNavigationFeedbackType, + description: String?, + source: FeedbackSource + ) async throws -> UserFeedback { + return try await sendNavigationFeedback( + feedback, + type: type, + description: description, + source: source + ) + } + + func sendPassiveNavigationFeedback( + _ feedback: FeedbackEvent, + type: PassiveNavigationFeedbackType, + description: String?, + source: FeedbackSource + ) async throws -> UserFeedback { + return try await sendNavigationFeedback( + feedback, + type: type, + description: description, + source: source + ) + } + + func sendNavigationFeedback( + _ feedback: FeedbackEvent, + type: FeedbackType, + description: String?, + source: FeedbackSource + ) async throws -> UserFeedback { + let feedbackMetadata = feedback.metadata + guard let userFeedbackMetadata = feedbackMetadata.userFeedbackMetadata else { + throw NavigationEventsManagerError.invalidData + } + + let userFeedback = makeUserFeedback( + feedbackMetadata: feedbackMetadata, + type: type, + description: description, + source: source + ) + return try await withCheckedThrowingContinuation { continuation in + telemetry.read().postUserFeedback( + for: userFeedbackMetadata, + userFeedback: userFeedback + ) { expected in + if expected.isValue(), let coordinate = expected.value { + let userFeedback: UserFeedback = .init( + description: description, + type: type, + source: source, + screenshot: feedbackMetadata.screenshot, + location: CLLocation(coordinate: coordinate.value) + ) + continuation.resume(returning: userFeedback) + } else if expected.isError(), let errorString = expected.error { + continuation + .resume(throwing: NavigationEventsManagerError.failedToSend(reason: errorString as String)) + } else { + continuation.resume(throwing: NavigationEventsManagerError.failedToSend(reason: "Unknown")) + } + } + } + } + + func sendCarPlayConnectEvent() { + telemetry.read().postOuterDeviceEvent(for: .connected) + } + + func sendCarPlayDisconnectEvent() { + telemetry.read().postOuterDeviceEvent(for: .disconnected) + } + + private func createNativeUserCallback( + feedbackMetadata: FeedbackMetadata, + continuation: UnsafeContinuation, + type: FeedbackType, + description: String?, + source: FeedbackSource + ) -> MapboxNavigationNative_Private.UserFeedbackCallback { + return { expected in + if expected.isValue(), let coordinate = expected.value { + let userFeedback: UserFeedback = .init( + description: description, + type: type, + source: source, + screenshot: feedbackMetadata.screenshot, + location: CLLocation(coordinate: coordinate.value) + ) + continuation.resume(returning: userFeedback) + } else if expected.isError(), let errorString = expected.error { + continuation.resume(throwing: NavigationEventsManagerError.failedToSend(reason: errorString as String)) + } + } + } + + private func createScreenshot(screenshotOption: FeedbackScreenshotOption) async -> String? { + let screenshot: UIImage? = switch screenshotOption { + case .automatic: + await captureScreen(scaledToFit: 250) + case .custom(let customScreenshot): + customScreenshot + } + return screenshot?.jpegData(compressionQuality: 0.2)?.base64EncodedString() + } + + private func makeUserFeedback( + feedbackMetadata: FeedbackMetadata, + type: FeedbackType, + description: String?, + source: FeedbackSource + ) -> MapboxNavigationNative_Private.UserFeedback { + var feedbackSubType: [String] = [] + if let subtypeKey = type.subtypeKey { + feedbackSubType.append(subtypeKey) + } + return .init( + feedbackType: type.typeKey, + feedbackSubType: feedbackSubType, + description: description ?? "", + screenshot: .init(jpeg: nil, base64: feedbackMetadata.screenshot) + ) + } +} + +extension String { + func toDataRef() -> DataRef? { + if let data = data(using: .utf8), + let encodedData = Data(base64Encoded: data) + { + return .init(data: encodedData) + } + return nil + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationTelemetryManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationTelemetryManager.swift new file mode 100644 index 000000000..f002ba379 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationTelemetryManager.swift @@ -0,0 +1,42 @@ +import CoreLocation +import Foundation + +public struct UserFeedback: @unchecked Sendable { + public let description: String? + public let type: FeedbackType + public let source: FeedbackSource + public let screenshot: String? + public let location: CLLocation +} + +/// The ``NavigationTelemetryManager`` is responsible for telemetry in Navigation. +protocol NavigationTelemetryManager: AnyObject, Sendable { + var userInfo: [String: String?]? { get set } + + func sendCarPlayConnectEvent() + + func sendCarPlayDisconnectEvent() + + func createFeedback(screenshotOption: FeedbackScreenshotOption) async -> FeedbackEvent? + + func sendActiveNavigationFeedback( + _ feedback: FeedbackEvent, + type: ActiveNavigationFeedbackType, + description: String?, + source: FeedbackSource + ) async throws -> UserFeedback + + func sendPassiveNavigationFeedback( + _ feedback: FeedbackEvent, + type: PassiveNavigationFeedbackType, + description: String?, + source: FeedbackSource + ) async throws -> UserFeedback + + func sendNavigationFeedback( + _ feedback: FeedbackEvent, + type: FeedbackType, + description: String?, + source: FeedbackSource + ) async throws -> UserFeedback +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Typealiases.swift b/ios/Classes/Navigation/MapboxNavigationCore/Typealiases.swift new file mode 100644 index 000000000..239c54186 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Typealiases.swift @@ -0,0 +1,32 @@ +import MapboxDirections + +/// Options determining the primary mode of transportation. +public typealias ProfileIdentifier = MapboxDirections.ProfileIdentifier +/// A ``Waypoint`` object indicates a location along a route. It may be the route’s origin or destination, or it may be +/// another location that the route visits. A waypoint object indicates the location’s geographic location along with +/// other optional information, such as a name or the user’s direction approaching the waypoint. +public typealias Waypoint = MapboxDirections.Waypoint +/// A ``CongestionLevel`` indicates the level of traffic congestion along a road segment relative to the normal flow of +/// traffic along that segment. You can color-code a route line according to the congestion level along each segment of +/// the route. +public typealias CongestionLevel = MapboxDirections.CongestionLevel +/// Option set that contains attributes of a road segment. +public typealias RoadClasses = MapboxDirections.RoadClasses + +/// An instruction about an upcoming ``RouteStep``’s maneuver, optimized for speech synthesis. +public typealias SpokenInstruction = MapboxDirections.SpokenInstruction +/// A visual instruction banner contains all the information necessary for creating a visual cue about a given +/// ``RouteStep``. +public typealias VisualInstructionBanner = MapboxDirections.VisualInstructionBanner +/// An error that occurs when calculating directions. +public typealias DirectionsError = MapboxDirections.DirectionsError + +/// A ``RouteLeg`` object defines a single leg of a route between two waypoints. If the overall route has only two +/// waypoints, it has a single ``RouteLeg`` object that covers the entire route. The route leg object includes +/// information about the leg, such as its name, distance, and expected travel time. Depending on the criteria used to +/// calculate the route, the route leg object may also include detailed turn-by-turn instructions. +public typealias RouteLeg = MapboxDirections.RouteLeg +/// A ``RouteStep`` object represents a single distinct maneuver along a route and the approach to the next maneuver. +/// The route step object corresponds to a single instruction the user must follow to complete a portion of the route. +/// For example, a step might require the user to turn then follow a road. +public typealias RouteStep = MapboxDirections.RouteStep diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Utils/NavigationLog.swift b/ios/Classes/Navigation/MapboxNavigationCore/Utils/NavigationLog.swift new file mode 100644 index 000000000..c2238ca0f --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Utils/NavigationLog.swift @@ -0,0 +1,58 @@ +import Foundation +import MapboxCommon_Private.MBXLog_Internal +import OSLog + +@_documentation(visibility: internal) +public typealias Log = NavigationLog + +@_documentation(visibility: internal) +public enum NavigationLog { + public typealias Category = NavigationLogCategory + private typealias Logger = MapboxCommon_Private.Log + + public static func debug(_ message: String, category: Category) { + Logger.debug(forMessage: message, category: category.rawLogCategory) + } + + public static func info(_ message: String, category: Category) { + Logger.info(forMessage: message, category: category.rawLogCategory) + } + + public static func warning(_ message: String, category: Category) { + Logger.warning(forMessage: message, category: category.rawLogCategory) + } + + public static func error(_ message: String, category: Category) { + Logger.error(forMessage: message, category: category.rawLogCategory) + } + + public static func fault(_ message: String, category: Category) { + let faultLog: OSLog = .init(subsystem: "com.mapbox.navigation", category: category.rawValue) + os_log("%{public}@", log: faultLog, type: .fault, message) + Logger.error(forMessage: message, category: category.rawLogCategory) + } + + @available(*, unavailable, message: "Use NavigationLog.debug(_:category:)") + public static func trace(_ message: String) {} +} + +@_documentation(visibility: internal) +public struct NavigationLogCategory: RawRepresentable, Sendable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public static let billing: Self = .init(rawValue: "billing") + public static let navigation: Self = .init(rawValue: "navigation") + public static let settings: Self = .init(rawValue: "settings") + public static let unimplementedMethods: Self = .init(rawValue: "unimplemented-methods") + public static let navigationUI: Self = .init(rawValue: "navigation-ui") + public static let carPlay: Self = .init(rawValue: "car-play") + public static let copilot: Self = .init(rawValue: "copilot") + + public var rawLogCategory: String { + "navigation-ios/\(rawValue)" + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Utils/ScreenCapture.swift b/ios/Classes/Navigation/MapboxNavigationCore/Utils/ScreenCapture.swift new file mode 100644 index 000000000..1986f7c41 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Utils/ScreenCapture.swift @@ -0,0 +1,47 @@ +import Foundation +#if os(iOS) +import UIKit + +extension UIWindow { + /// Returns a screenshot of the current window + public func capture() -> UIImage? { + UIGraphicsBeginImageContextWithOptions(frame.size, isOpaque, UIScreen.main.scale) + + drawHierarchy(in: bounds, afterScreenUpdates: false) + + guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil } + + UIGraphicsEndImageContext() + + return image + } +} + +extension UIImage { + func scaled(toFit newWidth: CGFloat) -> UIImage? { + let factor = newWidth / size.width + let newSize = CGSize(width: size.width * factor, height: size.height * factor) + + UIGraphicsBeginImageContext(newSize) + + draw(in: CGRect(origin: .zero, size: newSize)) + + guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil } + + UIGraphicsEndImageContext() + + return image + } +} + +#endif + +@MainActor +func captureScreen(scaledToFit width: CGFloat) -> UIImage? { +#if os(iOS) + return UIApplication.shared.windows.filter(\.isKeyWindow).first?.capture()?.scaled(toFit: width) +#else + + return nil // Not yet implemented for other platforms +#endif +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Utils/UnimplementedLogging.swift b/ios/Classes/Navigation/MapboxNavigationCore/Utils/UnimplementedLogging.swift new file mode 100644 index 000000000..5a5f9958e --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Utils/UnimplementedLogging.swift @@ -0,0 +1,98 @@ +import Dispatch +import Foundation +import OSLog + +/// Protocols that provide no-op default method implementations can use this protocol to log a message to the console +/// whenever an unimplemented delegate method is called. +/// +/// In Swift, optional protocol methods exist only for Objective-C compatibility. However, various protocols in this +/// library follow a classic Objective-C delegate pattern in which the protocol would have a number of optional methods. +/// Instead of the disallowed `optional` keyword, these protocols conform to the ``UnimplementedLogging`` protocol to +/// inform about unimplemented methods at runtime. These console messages are logged to the subsystem `com.mapbox.com` +/// with a category of the format “delegation.ProtocolName”, where ProtocolName is the name of the +/// protocol that defines the method. +/// +/// The default method implementations should be provided as part of the protocol or an extension thereof. If the +/// default implementations reside in an extension, the extension should have the same visibility level as the protocol +/// itself. +public protocol UnimplementedLogging { + /// Prints a warning to standard output. + /// - Parameters: + /// - protocolType: The type of the protocol to implement. + /// - level: The log level. + /// - function: The function name to be logged. + func logUnimplemented(protocolType: Any, level: OSLogType, function: String) +} + +extension UnimplementedLogging { + public func logUnimplemented(protocolType: Any, level: OSLogType, function: String = #function) { + let protocolDescription = String(describing: protocolType) + let selfDescription = String(describing: type(of: self)) + + let description = UnimplementedLoggingState.Description(typeDescription: selfDescription, function: function) + + guard _unimplementedLoggingState.markWarned(description) == .marked else { + return + } + + let logMethod: (String, NavigationLogCategory) -> Void = switch level { + case .debug, .info: + Log.info + case .fault: + Log.fault + case .error: + Log.error + default: + Log.warning + } + logMethod( + "Unimplemented delegate method in \(selfDescription): \(protocolDescription).\(function). This message will only be logged once.", + .unimplementedMethods + ) + } +} + +/// Contains a list of unimplemented log descriptions so that we won't log the same warnings twice. +/// Because this state is a global object and part of the public API it has synchronization primitive using a lock. +/// - Note: The type is safe to use from multiple threads. +final class UnimplementedLoggingState { + struct Description: Equatable { + let typeDescription: String + let function: String + } + + enum MarkingResult { + case alreadyMarked + case marked + } + + private let lock: NSLock = .init() + private var warned: [Description] = [] + + func markWarned(_ description: Description) -> MarkingResult { + lock.lock(); defer { + lock.unlock() + } + guard !warned.contains(description) else { + return .alreadyMarked + } + warned.append(description) + return .marked + } + + func clear() { + lock.lock(); defer { + lock.unlock() + } + warned.removeAll() + } + + func countWarned(forTypeDescription typeDescription: String) -> Int { + return warned + .filter { $0.typeDescription == typeDescription } + .count + } +} + +/// - Note: Exposed as internal to verify the behaviour in tests. +let _unimplementedLoggingState: UnimplementedLoggingState = .init() diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Utils/UserAgent.swift b/ios/Classes/Navigation/MapboxNavigationCore/Utils/UserAgent.swift new file mode 100644 index 000000000..495816641 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Utils/UserAgent.swift @@ -0,0 +1,100 @@ +import Foundation +import MapboxCommon_Private + +extension URLRequest { + public mutating func setNavigationUXUserAgent() { + setValue(.navigationUXUserAgent, forHTTPHeaderField: "User-Agent") + } +} + +extension String { + public static let navigationUXUserAgent: String = { + let processInfo = ProcessInfo() + let systemVersion = processInfo.operatingSystemVersion + let version = [ + systemVersion.majorVersion, + systemVersion.minorVersion, + systemVersion.patchVersion, + ].map(String.init).joined(separator: ".") + let system = processInfo.system() + + let systemComponent = [system, version].joined(separator: "/") + +#if targetEnvironment(simulator) + var simulator: String? = "Simulator" +#else + var simulator: String? +#endif + + let otherComponents = [ + processInfo.chip(), + simulator, + ].compactMap { $0 } + + let mainBundleId = Bundle.main.bundleIdentifier ?? Bundle.main.bundleURL.lastPathComponent + + let components = [ + "\(mainBundleId)/\(Bundle.main.version ?? "unknown")", + navigationUXUserAgentFragment, + systemComponent, + "(\(otherComponents.joined(separator: "; ")))", + ] + let userAgent = components.joined(separator: " ") + Log.info("UserAgent: \(userAgent)", category: .settings) + return userAgent + }() + + public static let navigationUXUserAgentFragment: String = + "\(Bundle.resolvedNavigationSDKName)/\(Bundle.mapboxNavigationVersion)" +} + +extension Bundle { + public static let navigationUXName: String = "mapbox-navigationUX-ios" + public static let navigationUIKitName: String = "mapbox-navigationUIKit-ios" + public static let navigationCoreName: String = "mapbox-navigationCore-ios" + /// Deduced SDK name. + /// + /// Equals ``navigationCoreName``, ``navigationUIKitName`` or ``navigationUXName``, based on the detected project + /// dependencies structure. + public static var resolvedNavigationSDKName: String { + if NSClassFromString("NavigationUX") != nil { + navigationUXName + } else if NSClassFromString("NavigationViewController") != nil { + navigationUIKitName + } else { + navigationCoreName + } + } + + var version: String? { + infoDictionary?["CFBundleShortVersionString"] as? String + } +} + +extension ProcessInfo { + fileprivate func chip() -> String { +#if arch(x86_64) + "x86_64" +#elseif arch(arm) + "arm" +#elseif arch(arm64) + "arm64" +#elseif arch(i386) + "i386" +#endif + } + + fileprivate func system() -> String { +#if os(OSX) + "macOS" +#elseif os(iOS) + "iOS" +#elseif os(watchOS) + "watchOS" +#elseif os(tvOS) + "tvOS" +#elseif os(Linux) + "Linux" +#endif + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Version.swift b/ios/Classes/Navigation/MapboxNavigationCore/Version.swift new file mode 100644 index 000000000..25590f1bb --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/Version.swift @@ -0,0 +1,6 @@ +import Foundation + +extension Bundle { + public static let mapboxNavigationVersion: String = "3.5.0" + public static let mapboxNavigationUXBundleIdentifier: String = "com.mapbox.navigationUX" +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerClient.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerClient.swift new file mode 100644 index 000000000..be5ff2907 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerClient.swift @@ -0,0 +1,93 @@ +@preconcurrency import AVFoundation + +struct AudioPlayerClient: Sendable { + var play: @Sendable (_ url: URL) async throws -> Bool + var load: @Sendable (_ sounds: [URL]) async throws -> Void +} + +@globalActor actor AudioPlayerActor { + static let shared = AudioPlayerActor() +} + +extension AudioPlayerClient { + @AudioPlayerActor + static func liveValue() -> AudioPlayerClient { + let audioActor = AudioActor() + return Self( + play: { sound in + try await audioActor.play(sound: sound) + }, + load: { sounds in + try await audioActor.load(sounds: sounds) + } + ) + } +} + +private actor AudioActor { + enum Failure: Error { + case soundIsPlaying(URL) + case soundNotLoaded(URL) + case soundsNotLoaded([URL: Error]) + } + + var players: [URL: AVAudioPlayer] = [:] + + func load(sounds: [URL]) throws { + let sounds = sounds.filter { !players.keys.contains($0) } + var errors: [URL: Error] = [:] + for sound in sounds { + do { + let player = try AVAudioPlayer(contentsOf: sound) + players[sound] = player + } catch { + errors[sound] = error + } + } + + guard errors.isEmpty else { + throw Failure.soundsNotLoaded(errors) + } + } + + func play(sound: URL) async throws -> Bool { + guard let player = players[sound] else { + throw Failure.soundNotLoaded(sound) + } + + guard !player.isPlaying else { + throw Failure.soundIsPlaying(sound) + } + + let stream = AsyncThrowingStream { continuation in + let delegate = Delegate(continuation: continuation) + player.delegate = delegate + continuation.onTermination = { _ in + player.stop() + player.currentTime = 0 + _ = delegate + } + + player.play() + } + + return try await stream.first(where: { @Sendable _ in true }) ?? false + } +} + +private final class Delegate: NSObject, AVAudioPlayerDelegate, Sendable { + let continuation: AsyncThrowingStream.Continuation + + init(continuation: AsyncThrowingStream.Continuation) { + self.continuation = continuation + } + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + continuation.yield(flag) + continuation.finish() + } + + func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { + continuation.finish(throwing: error) + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerDelegate.swift new file mode 100644 index 000000000..f1ea6d4af --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerDelegate.swift @@ -0,0 +1,11 @@ +import AVFAudio + +@MainActor +final class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate { + var onAudioPlayerDidFinishPlaying: (@MainActor (AVAudioPlayer, _ didFinishSuccessfully: Bool) -> Void)? + nonisolated func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + MainActor.assumingIsolated { + onAudioPlayerDidFinishPlaying?(player, flag) + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MapboxSpeechSynthesizer.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MapboxSpeechSynthesizer.swift new file mode 100644 index 000000000..8e31e42a5 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MapboxSpeechSynthesizer.swift @@ -0,0 +1,381 @@ +import _MapboxNavigationHelpers +import AVFoundation +import Combine +import MapboxDirections + +@MainActor +/// ``SpeechSynthesizing`` implementation, using Mapbox Voice API. Uses pre-caching mechanism for upcoming instructions. +public final class MapboxSpeechSynthesizer: SpeechSynthesizing { + private var _voiceInstructions: PassthroughSubject = .init() + public var voiceInstructions: AnyPublisher { + _voiceInstructions.eraseToAnyPublisher() + } + + // MARK: Speech Configuration + + public var muted: Bool = false { + didSet { + updatePlayerVolume(audioPlayer) + } + } + + private var volumeSubscribtion: AnyCancellable? + public var volume: VolumeMode = .system { + didSet { + guard volume != oldValue else { return } + + switch volume { + case .system: + subscribeToSystemVolume() + case .override(let volume): + volumeSubscribtion = nil + audioPlayer?.volume = volume + } + } + } + + private var currentVolume: Float { + switch volume { + case .system: + return 1.0 + case .override(let volume): + return volume + } + } + + private func subscribeToSystemVolume() { + audioPlayer?.volume = AVAudioSession.sharedInstance().outputVolume + volumeSubscribtion = AVAudioSession.sharedInstance().publisher(for: \.outputVolume).sink { [weak self] volume in + self?.audioPlayer?.volume = volume + } + } + + public var locale: Locale? = Locale.autoupdatingCurrent + + /// Number of upcoming `Instructions` to be pre-fetched. + /// + /// Higher number may exclude cases when required vocalization data is not yet loaded, but also will increase + /// network consumption at the beginning of the route. Keep in mind that pre-fetched instuctions are not guaranteed + /// to be vocalized at all due to re-routing or user actions. "0" will effectively disable pre-fetching. + public var stepsAheadToCache: UInt = 3 + + /// An `AVAudioPlayer` through which spoken instructions are played. + private var audioPlayer: AVAudioPlayer? { + _audioPlayer?.audioPlayer + } + + private var _audioPlayer: SendableAudioPlayer? + private let audioPlayerDelegate: AudioPlayerDelegate = .init() + + /// Controls if this speech synthesizer is allowed to manage the shared `AVAudioSession`. + /// Set this field to `false` if you want to manage the session yourself, for example if your app has background + /// music. + /// Default value is `true`. + public var managesAudioSession: Bool = true + + /// Mapbox speech engine instance. + /// + /// The speech synthesizer uses this object to convert instruction text to audio. + private(set) var remoteSpeechSynthesizer: SpeechSynthesizer + + private var cache: SyncBimodalCache + private var audioTask: Task? + + private var previousInstruction: SpokenInstruction? + + // MARK: Instructions vocalization + + /// Checks if speech synthesizer is now pronouncing an instruction. + public var isSpeaking: Bool { + return audioPlayer?.isPlaying ?? false + } + + /// Creates new `MapboxSpeechSynthesizer` with standard `SpeechSynthesizer` for converting text to audio. + /// + /// - parameter accessToken: A Mapbox [access token](https://www.mapbox.com/help/define-access-token/) used to + /// authorize Mapbox Voice API requests. If an access token is not specified when initializing the speech + /// synthesizer object, it should be specified in the `MBXAccessToken` key in the main application bundle’s + /// Info.plist. + /// - parameter host: An optional hostname to the server API. The Mapbox Voice API endpoint is used by default. + init( + apiConfiguration: ApiConfiguration, + skuTokenProvider: SkuTokenProvider + ) { + self.cache = MapboxSyncBimodalCache() + + self.remoteSpeechSynthesizer = SpeechSynthesizer( + apiConfiguration: apiConfiguration, + skuTokenProvider: skuTokenProvider + ) + + subscribeToSystemVolume() + } + + deinit { + Task { @MainActor [_audioPlayer] in + _audioPlayer?.stop() + } + } + + public func prepareIncomingSpokenInstructions(_ instructions: [SpokenInstruction], locale: Locale? = nil) { + guard let locale = locale ?? self.locale else { + _voiceInstructions.send( + VoiceInstructionEvents.EncounteredError( + error: SpeechError.undefinedSpeechLocale( + instruction: instructions.first! + ) + ) + ) + return + } + + for insturction in instructions.prefix(Int(stepsAheadToCache)) { + if !hasCachedSpokenInstructionForKey(insturction.ssmlText, with: locale) { + downloadAndCacheSpokenInstruction(instruction: insturction, locale: locale) + } + } + } + + public func speak(_ instruction: SpokenInstruction, during legProgress: RouteLegProgress, locale: Locale? = nil) { + guard !muted else { return } + guard let locale = locale ?? self.locale else { + _voiceInstructions.send( + VoiceInstructionEvents.EncounteredError( + error: SpeechError.undefinedSpeechLocale( + instruction: instruction + ) + ) + ) + return + } + + guard let data = cachedDataForKey(instruction.ssmlText, with: locale) else { + fetchAndSpeak(instruction: instruction, locale: locale) + return + } + + _voiceInstructions.send( + VoiceInstructionEvents.WillSpeak( + instruction: instruction + ) + ) + safeDuckAudio(instruction: instruction) + speak(instruction, data: data) + } + + public func stopSpeaking() { + audioPlayer?.stop() + } + + public func interruptSpeaking() { + audioPlayer?.stop() + } + + /// Vocalize the provided audio data. + /// + /// This method is a final part of a vocalization pipeline. It passes audio data to the audio player. `instruction` + /// is used mainly for logging and reference purposes. It's text contents do not affect the vocalization while the + /// actual audio is passed via `data`. + /// - parameter instruction: corresponding instruction to be vocalized. Used for logging and reference. Modifying + /// it's `text` or `ssmlText` does not affect vocalization. + /// - parameter data: audio data, as provided by `remoteSpeechSynthesizer`, to be played. + public func speak(_ instruction: SpokenInstruction, data: Data) { + if let audioPlayer { + if let previousInstruction, audioPlayer.isPlaying { + _voiceInstructions.send( + VoiceInstructionEvents.DidInterrupt( + interruptedInstruction: previousInstruction, + interruptingInstruction: instruction + ) + ) + } + + deinitAudioPlayer() + } + + switch safeInitializeAudioPlayer( + data: data, + instruction: instruction + ) { + case .success(let player): + _audioPlayer = .init(player) + previousInstruction = instruction + audioPlayer?.play() + case .failure(let error): + safeUnduckAudio(instruction: instruction) + _voiceInstructions.send( + VoiceInstructionEvents.EncounteredError( + error: error + ) + ) + } + } + + // MARK: Private Methods + + /// Fetches and plays an instruction. + private func fetchAndSpeak(instruction: SpokenInstruction, locale: Locale) { + audioTask?.cancel() + + _voiceInstructions.send( + VoiceInstructionEvents.WillSpeak( + instruction: instruction + ) + ) + let ssmlText = instruction.ssmlText + let options = SpeechOptions(ssml: ssmlText, locale: locale) + + audioTask = Task { + do { + let audio = try await self.remoteSpeechSynthesizer.audioData(with: options) + try Task.checkCancellation() + self.cache(audio, forKey: ssmlText, with: locale) + self.safeDuckAudio(instruction: instruction) + self.speak( + instruction, + data: audio + ) + } catch let speechError as SpeechErrorApiError { + switch speechError { + case .transportError(underlying: let urlError) where urlError.code == .cancelled: + // Since several voice instructions might be received almost at the same time cancelled + // URLSessionDataTask is not considered as error. + // This means that in this case fallback to another speech synthesizer will not be performed. + break + default: + self._voiceInstructions.send( + VoiceInstructionEvents.EncounteredError( + error: SpeechError.apiError( + instruction: instruction, + options: options, + underlying: speechError + ) + ) + ) + } + } + } + } + + private func downloadAndCacheSpokenInstruction(instruction: SpokenInstruction, locale: Locale) { + let ssmlText = instruction.ssmlText + let options = SpeechOptions(ssml: ssmlText, locale: locale) + + Task { + do { + let audio = try await remoteSpeechSynthesizer.audioData(with: options) + cache(audio, forKey: ssmlText, with: locale) + } catch { + Log.error( + "Couldn't cache spoken instruction '\(instruction)' due to error \(error) ", + category: .navigation + ) + } + } + } + + func safeDuckAudio(instruction: SpokenInstruction?) { + guard managesAudioSession else { return } + if let error = AVAudioSession.sharedInstance().tryDuckAudio() { + _voiceInstructions.send( + VoiceInstructionEvents.EncounteredError( + error: SpeechError.unableToControlAudio( + instruction: instruction, + action: .duck, + underlying: error + ) + ) + ) + } + } + + func safeUnduckAudio(instruction: SpokenInstruction?) { + guard managesAudioSession else { return } + if let error = AVAudioSession.sharedInstance().tryUnduckAudio() { + _voiceInstructions.send( + VoiceInstructionEvents.EncounteredError( + error: SpeechError.unableToControlAudio( + instruction: instruction, + action: .unduck, + underlying: error + ) + ) + ) + } + } + + private func cache(_ data: Data, forKey key: String, with locale: Locale) { + cache.store( + data: data, + key: locale.identifier + key, + mode: [.InMemory, .OnDisk] + ) + } + + private func cachedDataForKey(_ key: String, with locale: Locale) -> Data? { + return cache[locale.identifier + key] + } + + private func hasCachedSpokenInstructionForKey(_ key: String, with locale: Locale) -> Bool { + return cachedDataForKey(key, with: locale) != nil + } + + private func updatePlayerVolume(_ player: AVAudioPlayer?) { + player?.volume = muted ? 0.0 : currentVolume + } + + private func safeInitializeAudioPlayer( + data: Data, + instruction: SpokenInstruction + ) -> Result { + do { + let player = try AVAudioPlayer(data: data) + player.delegate = audioPlayerDelegate + audioPlayerDelegate.onAudioPlayerDidFinishPlaying = { [weak self] _, _ in + guard let self else { return } + safeUnduckAudio(instruction: previousInstruction) + + guard let instruction = previousInstruction else { + assertionFailure("Speech Synthesizer finished speaking 'nil' instruction") + return + } + + _voiceInstructions.send( + VoiceInstructionEvents.DidSpeak( + instruction: instruction + ) + ) + } + updatePlayerVolume(player) + + return .success(player) + } catch { + return .failure(SpeechError.unableToInitializePlayer( + playerType: AVAudioPlayer.self, + instruction: instruction, + synthesizer: remoteSpeechSynthesizer, + underlying: error + )) + } + } + + private func deinitAudioPlayer() { + audioPlayer?.stop() + audioPlayer?.delegate = nil + } +} + +@MainActor +private final class SendableAudioPlayer: Sendable { + let audioPlayer: AVAudioPlayer + + init(_ audioPlayer: AVAudioPlayer) { + self.audioPlayer = audioPlayer + } + + nonisolated func stop() { + DispatchQueue.main.async { + self.audioPlayer.stop() + } + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MultiplexedSpeechSynthesizer.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MultiplexedSpeechSynthesizer.swift new file mode 100644 index 000000000..97535cfe8 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MultiplexedSpeechSynthesizer.swift @@ -0,0 +1,181 @@ +import _MapboxNavigationHelpers +import AVFoundation +import Combine +import MapboxDirections + +/// ``SpeechSynthesizing``implementation, aggregating other implementations, to allow 'fallback' mechanism. +/// Can be initialized with array of synthesizers which will be called in order of appearance, until one of them is +/// capable to vocalize current ``SpokenInstruction`` +public final class MultiplexedSpeechSynthesizer: SpeechSynthesizing { + private static let mutedDefaultKey = "com.mapbox.navigation.MultiplexedSpeechSynthesizer.isMuted" + private var _voiceInstructions: PassthroughSubject = .init() + public var voiceInstructions: AnyPublisher { + _voiceInstructions.eraseToAnyPublisher() + } + + // MARK: Speech Configuration + + public var muted: Bool = false { + didSet { + applyMute() + } + } + + public var volume: VolumeMode = .system { + didSet { + applyVolume() + } + } + + public var locale: Locale? = Locale.autoupdatingCurrent { + didSet { + applyLocale() + } + } + + private func applyMute() { + UserDefaults.standard.setValue(muted, forKey: Self.mutedDefaultKey) + speechSynthesizers.forEach { $0.muted = muted } + } + + private func applyVolume() { + speechSynthesizers.forEach { $0.volume = volume } + } + + private func applyLocale() { + speechSynthesizers.forEach { $0.locale = locale } + } + + /// Controls if this speech synthesizer is allowed to manage the shared `AVAudioSession`. + /// Set this field to `false` if you want to manage the session yourself, for example if your app has background + /// music. + /// Default value is `true`. + public var managesAudioSession: Bool { + get { + speechSynthesizers.allSatisfy { $0.managesAudioSession == true } + } + set { + speechSynthesizers.forEach { $0.managesAudioSession = newValue } + } + } + + // MARK: Instructions vocalization + + public var isSpeaking: Bool { + return speechSynthesizers.first(where: { $0.isSpeaking }) != nil + } + + private var synthesizersSubscriptions: [AnyCancellable] = [] + public var speechSynthesizers: [any SpeechSynthesizing] { + willSet { + upstreamSynthesizersWillUpdate(newValue) + } + didSet { + upstreamSynthesizersUpdated() + } + } + + private var currentLegProgress: RouteLegProgress? + private var currentInstruction: SpokenInstruction? + + public init(speechSynthesizers: [any SpeechSynthesizing]) { + self.speechSynthesizers = speechSynthesizers + applyVolume() + postInit() + } + + public convenience init( + mapboxSpeechApiConfiguration: ApiConfiguration, + skuTokenProvider: @Sendable @escaping () -> String?, + customSpeechSynthesizers: [SpeechSynthesizing] = [] + ) { + var speechSynthesizers = customSpeechSynthesizers + speechSynthesizers.append(MapboxSpeechSynthesizer( + apiConfiguration: mapboxSpeechApiConfiguration, + skuTokenProvider: .init(skuToken: skuTokenProvider) + )) + speechSynthesizers.append(SystemSpeechSynthesizer()) + self.init(speechSynthesizers: speechSynthesizers) + } + + private func postInit() { + muted = UserDefaults.standard.bool(forKey: Self.mutedDefaultKey) + upstreamSynthesizersWillUpdate(speechSynthesizers) + upstreamSynthesizersUpdated() + } + + public func prepareIncomingSpokenInstructions(_ instructions: [SpokenInstruction], locale: Locale? = nil) { + speechSynthesizers.forEach { $0.prepareIncomingSpokenInstructions(instructions, locale: locale) } + } + + public func speak(_ instruction: SpokenInstruction, during legProgress: RouteLegProgress, locale: Locale? = nil) { + currentLegProgress = legProgress + currentInstruction = instruction + speechSynthesizers.first?.speak(instruction, during: legProgress, locale: locale) + } + + public func stopSpeaking() { + speechSynthesizers.forEach { $0.stopSpeaking() } + } + + public func interruptSpeaking() { + speechSynthesizers.forEach { $0.interruptSpeaking() } + } + + private func upstreamSynthesizersWillUpdate(_ newValue: [any SpeechSynthesizing]) { + var found: [any SpeechSynthesizing] = [] + let duplicate = newValue.first { newSynth in + if found.first(where: { + return $0 === newSynth + }) == nil { + found.append(newSynth) + return false + } + return true + } + + precondition( + duplicate == nil, + "Single `SpeechSynthesizing` object passed to `MultiplexedSpeechSynthesizer` multiple times!" + ) + + speechSynthesizers.forEach { + $0.interruptSpeaking() + } + synthesizersSubscriptions = [] + } + + private func upstreamSynthesizersUpdated() { + synthesizersSubscriptions = speechSynthesizers.enumerated().map { item in + + return item.element.voiceInstructions.sink { [weak self] event in + switch event { + case let errorEvent as VoiceInstructionEvents.EncounteredError: + switch errorEvent.error { + case .unableToControlAudio(instruction: _, action: _, underlying: _): + // do nothing special + break + default: + if let legProgress = self?.currentLegProgress, + let currentInstruction = self?.currentInstruction, + item.offset + 1 < self?.speechSynthesizers.count ?? 0 + { + self?.speechSynthesizers[item.offset + 1].speak( + currentInstruction, + during: legProgress, + locale: self?.locale + ) + return + } + } + default: + break + } + self?._voiceInstructions.send(event) + } + } + applyMute() + applyVolume() + applyLocale() + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/RouteVoiceController.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/RouteVoiceController.swift new file mode 100644 index 000000000..d8651af0a --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/RouteVoiceController.swift @@ -0,0 +1,150 @@ +import AVFoundation +import Combine +import Foundation +import UIKit + +@MainActor +public final class RouteVoiceController { + public internal(set) var speechSynthesizer: SpeechSynthesizing + var subscriptions: Set = [] + + /// If true, a noise indicating the user is going to be rerouted will play prior to rerouting. + public var playsRerouteSound: Bool = true + + public init( + routeProgressing: AnyPublisher, + rerouteStarted: AnyPublisher, + fasterRouteSet: AnyPublisher, + speechSynthesizer: SpeechSynthesizing + ) { + self.speechSynthesizer = speechSynthesizer + loadSounds() + + routeProgressing + .sink { [weak self] state in + self?.handle(routeProgressState: state) + } + .store(in: &subscriptions) + + rerouteStarted + .sink { [weak self] in + Task { [weak self] in + await self?.playReroutingSound() + } + } + .store(in: &subscriptions) + + fasterRouteSet + .sink { [weak self] in + Task { [weak self] in + await self?.playReroutingSound() + } + } + .store(in: &subscriptions) + + verifyBackgroundAudio() + } + + private func verifyBackgroundAudio() { + Task { + guard UIApplication.shared.isKind(of: UIApplication.self) else { + return + } + + if !Bundle.main.backgroundModes.contains("audio") { + assertionFailure( + "This application’s Info.plist file must include “audio” in UIBackgroundModes. This background mode is used for spoken instructions while the application is in the background." + ) + } + } + } + + private func handle(routeProgressState: RouteProgressState?) { + guard let routeProgress = routeProgressState?.routeProgress, + let spokenInstruction = routeProgressState?.routeProgress.currentLegProgress.currentStepProgress + .currentSpokenInstruction + else { + return + } + + // AVAudioPlayer is flacky on simulator as of iOS 17.1, this is a workaround for UI tests + guard ProcessInfo.processInfo.environment["isUITest"] == nil else { return } + + let locale = routeProgress.route.speechLocale + + var remainingSpokenInstructions = routeProgressState?.routeProgress.currentLegProgress.currentStepProgress + .remainingSpokenInstructions ?? [] + let nextStepInstructions = routeProgressState?.routeProgress.upcomingLeg?.steps.first? + .instructionsSpokenAlongStep + remainingSpokenInstructions.append(contentsOf: nextStepInstructions ?? []) + if !remainingSpokenInstructions.isEmpty { + speechSynthesizer.prepareIncomingSpokenInstructions( + remainingSpokenInstructions, + locale: locale + ) + } + + speechSynthesizer.locale = locale + speechSynthesizer.speak( + spokenInstruction, + during: routeProgress.currentLegProgress, + locale: locale + ) + } + + private func playReroutingSound() async { + guard playsRerouteSound, !speechSynthesizer.muted else { + return + } + + speechSynthesizer.stopSpeaking() + + guard let rerouteSoundUrl = Bundle.mapboxNavigationUXCore.rerouteSoundUrl else { + return + } + + if let error = AVAudioSession.sharedInstance().tryDuckAudio() { + Log.error("Failed to duck sound for reroute with error: \(error)", category: .navigation) + } + + defer { + if let error = AVAudioSession.sharedInstance().tryUnduckAudio() { + Log.error("Failed to unduck sound for reroute with error: \(error)", category: .navigation) + } + } + + do { + let successful = try await Current.audioPlayerClient.play(rerouteSoundUrl) + if !successful { + Log.error("Failed to play sound for reroute", category: .navigation) + } + } catch { + Log.error("Failed to play sound for reroute with error: \(error)", category: .navigation) + } + } + + private func loadSounds() { + Task { + do { + guard let rerouteSoundUrl = Bundle.mapboxNavigationUXCore.rerouteSoundUrl else { return } + try await Current.audioPlayerClient.load([rerouteSoundUrl]) + } catch { + Log.error("Failed to load sound for reroute", category: .navigation) + } + } + } +} + +extension Bundle { + fileprivate var rerouteSoundUrl: URL? { + guard let rerouteSoundUrl = url( + forResource: "reroute-sound", + withExtension: "pcm" + ) else { + Log.error("Failed to find audio file for reroute", category: .navigation) + return nil + } + + return rerouteSoundUrl + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/Speech.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/Speech.swift new file mode 100644 index 000000000..5cb5930fa --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/Speech.swift @@ -0,0 +1,201 @@ +import _MapboxNavigationHelpers +import Foundation + +/// A `SpeechSynthesizer` object converts text into spoken audio. Unlike `AVSpeechSynthesizer`, a `SpeechSynthesizer` +/// object produces audio by sending an HTTP request to the Mapbox Voice API, which produces more natural-sounding audio +/// in various languages. With a speech synthesizer object, you can asynchronously generate audio data based on the +/// ``SpeechOptions`` object you provide, or you can get the URL used to make this request. +/// +/// Use `AVAudioPlayer` to play the audio that a speech synthesizer object produces. +struct SpeechSynthesizer: Sendable { + private let apiConfiguration: ApiConfiguration + private let skuTokenProvider: SkuTokenProvider + private let urlSession: URLSession + + // MARK: Creating a Speech Object + + init( + apiConfiguration: ApiConfiguration, + skuTokenProvider: SkuTokenProvider, + urlSession: URLSession = .shared + ) { + self.apiConfiguration = apiConfiguration + self.skuTokenProvider = skuTokenProvider + self.urlSession = urlSession + } + + // MARK: Getting Speech + + @discardableResult + /// Asynchronously fetches the audio file. + /// This method retrieves the audio asynchronously over a network connection. If a connection error or server error + /// occurs, details about the error are passed into the given completion handler in lieu of the audio file. + /// - Parameter options: A ``SpeechOptions`` object specifying the requirements for the resulting audio file. + /// - Returns: The audio data. + func audioData(with options: SpeechOptions) async throws -> Data { + try await data(with: url(forSynthesizing: options)) + } + + /// Returns a URL session task for the given URL that will run the given closures on completion or error. + /// - Parameter url: The URL to request. + /// - Returns: ``SpeechErrorApiError`` + private func data( + with url: URL + ) async throws -> Data { + var request = URLRequest(url: url) + request.setNavigationUXUserAgent() + + do { + let (data, response) = try await urlSession.data(for: request) + try validateResponse(response, data: data) + return data + } catch let error as SpeechErrorApiError { + throw error + } catch let urlError as URLError { + throw SpeechErrorApiError.transportError(underlying: urlError) + } catch { + throw SpeechErrorApiError.unknownError(underlying: error) + } + } + + /// The HTTP URL used to fetch audio from the API. + private func url(forSynthesizing options: SpeechOptions) -> URL { + var params = options.params + + params.append(apiConfiguration.accessTokenUrlQueryItem()) + + if let skuToken = skuTokenProvider.skuToken() { + params += [URLQueryItem(name: "sku", value: skuToken)] + } + + let unparameterizedURL = URL(string: options.path, relativeTo: apiConfiguration.endPoint)! + var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! + components.queryItems = params + return components.url! + } + + private func validateResponse(_ response: URLResponse, data: Data) throws { + guard response.mimeType == "application/json" else { return } + + let decoder = JSONDecoder() + let serverErrorResponse = try decoder.decode(ServerErrorResponse.self, from: data) + if serverErrorResponse.code == "Ok" || (serverErrorResponse.code == nil && serverErrorResponse.message == nil) { + return + } + try Self.parserServerError( + response: response, + serverErrorResponse: serverErrorResponse + ) + } + + /// Returns an error that supplements the given underlying error with additional information from the an HTTP + /// response’s body or headers. + static func parserServerError( + response: URLResponse, + serverErrorResponse: ServerErrorResponse + ) throws { + guard let response = response as? HTTPURLResponse else { + throw SpeechErrorApiError.serverError(response, serverErrorResponse) + } + + switch response.statusCode { + case 429: + throw SpeechErrorApiError.rateLimited( + rateLimitInterval: response.rateLimitInterval, + rateLimit: response.rateLimit, + resetTime: response.rateLimitResetTime + ) + default: + throw SpeechErrorApiError.serverError(response, serverErrorResponse) + } + } +} + +enum SpeechErrorApiError: LocalizedError { + case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) + case transportError(underlying: URLError) + case unknownError(underlying: Error) + case serverError(URLResponse, ServerErrorResponse) + + var failureReason: String? { + switch self { + case .transportError(underlying: let urlError): + return urlError.userInfo[NSLocalizedFailureReasonErrorKey] as? String + case .rateLimited(rateLimitInterval: let interval, rateLimit: let limit, _): + let intervalFormatter = DateComponentsFormatter() + intervalFormatter.unitsStyle = .full + guard let interval, let limit else { + return "Too many requests." + } + let formattedInterval = intervalFormatter.string(from: interval) ?? "\(interval) seconds" + let formattedCount = NumberFormatter.localizedString(from: NSNumber(value: limit), number: .decimal) + return "More than \(formattedCount) requests have been made with this access token within a period of \(formattedInterval)." + case .serverError(let response, let serverResponse): + if let serverMessage = serverResponse.message { + return serverMessage + } else if let httpResponse = response as? HTTPURLResponse { + return HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode) + } else if let code = serverResponse.code, + let serverStatusCode = Int(code) + { + return HTTPURLResponse.localizedString(forStatusCode: serverStatusCode) + } else { + return "Server error" + } + case .unknownError(underlying: let error as NSError): + return error.userInfo[NSLocalizedFailureReasonErrorKey] as? String + } + } + + var recoverySuggestion: String? { + switch self { + case .transportError(underlying: let urlError): + return urlError.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String + case .rateLimited(rateLimitInterval: _, rateLimit: _, resetTime: let rolloverTime): + guard let rolloverTime else { + return nil + } + let formattedDate: String = DateFormatter.localizedString( + from: rolloverTime, + dateStyle: .long, + timeStyle: .long + ) + return "Wait until \(formattedDate) before retrying." + case .serverError: + return nil + case .unknownError(underlying: let error as NSError): + return error.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String + } + } +} + +extension HTTPURLResponse { + var rateLimit: UInt? { + guard let limit = allHeaderFields["X-Rate-Limit-Limit"] as? String else { + return nil + } + return UInt(limit) + } + + var rateLimitInterval: TimeInterval? { + guard let interval = allHeaderFields["X-Rate-Limit-Interval"] as? String else { + return nil + } + return TimeInterval(interval) + } + + var rateLimitResetTime: Date? { + guard let resetTime = allHeaderFields["X-Rate-Limit-Reset"] as? String else { + return nil + } + guard let resetTimeNumber = Double(resetTime) else { + return nil + } + return Date(timeIntervalSince1970: resetTimeNumber) + } +} + +struct ServerErrorResponse: Decodable { + let code: String? + let message: String? +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechError.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechError.swift new file mode 100644 index 000000000..2c03609f5 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechError.swift @@ -0,0 +1,60 @@ +import AVKit +import Foundation +import MapboxDirections + +/// The speech-related action that failed. +/// - Seealso: ``SpeechError``. +public enum SpeechFailureAction: String, Sendable { + /// A failure occurred while attempting to mix audio. + case mix + /// A failure occurred while attempting to duck audio. + case duck + /// A failure occurred while attempting to unduck audio. + case unduck + /// A failure occurred while attempting to play audio. + case play +} + +/// A error type returned when encountering errors in the speech engine. +public enum SpeechError: LocalizedError { + /// An error occurred when requesting speech assets from a server API. + /// - Parameters: + /// - instruction: the instruction that failed. + /// - options: the SpeechOptions that were used to make the API request. + /// - underlying: the underlying `Error` returned by the API. + case apiError(instruction: SpokenInstruction, options: SpeechOptions, underlying: Error?) + + /// The speech engine did not fail with the error itself, but did not provide actual data to vocalize. + /// - Parameters: + /// - instruction: the instruction that failed. + /// - options: the SpeechOptions that were used to make the API request. + case noData(instruction: SpokenInstruction, options: SpeechOptions) + + /// The speech engine was unable to perform an action on the system audio service. + /// - Parameters: + /// - instruction: The instruction that failed. + /// - action: a `SpeechFailureAction` that describes the action attempted. + /// - underlying: the `Error` that was optrionally returned by the audio service. + case unableToControlAudio(instruction: SpokenInstruction?, action: SpeechFailureAction, underlying: Error?) + + /// The speech engine was unable to initalize an audio player. + /// - Parameters: + /// - playerType: the type of `AVAudioPlayer` that failed to initalize. + /// - instruction: The instruction that failed. + /// - synthesizer: The speech engine that attempted the initalization. + /// - underlying: the `Error` that was returned by the system audio service. + case unableToInitializePlayer( + playerType: AVAudioPlayer.Type, + instruction: SpokenInstruction, + synthesizer: Sendable?, + underlying: Error + ) + + /// There was no `Locale` provided during processing instruction. + /// - parameter instruction: The instruction that failed. + case undefinedSpeechLocale(instruction: SpokenInstruction) + + /// The speech engine does not support provided locale + /// - parameter locale: Offending locale. + case unsupportedLocale(locale: Locale) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechOptions.swift new file mode 100644 index 000000000..ae5a25d29 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechOptions.swift @@ -0,0 +1,82 @@ +import Foundation + +public enum TextType: String, Codable, Sendable, Hashable { + case text + case ssml +} + +public enum AudioFormat: String, Codable, Sendable, Hashable { + case mp3 +} + +public enum SpeechGender: String, Codable, Sendable, Hashable { + case female + case male + case neuter +} + +public struct SpeechOptions: Codable, Sendable, Equatable { + public init( + text: String, + locale: Locale + ) { + self.text = text + self.locale = locale + self.textType = .text + } + + public init( + ssml: String, + locale: Locale + ) { + self.text = ssml + self.locale = locale + self.textType = .ssml + } + + /// `String` to create audiofile for. Can either be plain text or + /// [`SSML`](https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language). + /// + /// If `SSML` is provided, `TextType` must be ``TextType/ssml``. + public var text: String + + /// Type of text to synthesize. + /// + /// `SSML` text must be valid `SSML` for request to work. + public let textType: TextType + + /// Audio format for outputted audio file. + public var outputFormat: AudioFormat = .mp3 + + /// The locale in which the audio is spoken. + /// + /// By default, the user's system locale will be used to decide upon an appropriate voice. + public var locale: Locale + + /// Gender of voice speaking text. + /// + /// - Note: not all languages have male and female voices. + public var speechGender: SpeechGender = .neuter + + /// The path of the request URL, not including the hostname or any parameters. + var path: String { + var characterSet = CharacterSet.urlPathAllowed + characterSet.remove(charactersIn: "/") + return "voice/v1/speak/\(text.addingPercentEncoding(withAllowedCharacters: characterSet)!)" + } + + /// An array of URL parameters to include in the request URL. + var params: [URLQueryItem] { + var params: [URLQueryItem] = [ + URLQueryItem(name: "textType", value: String(describing: textType)), + URLQueryItem(name: "language", value: locale.identifier), + URLQueryItem(name: "outputFormat", value: String(describing: outputFormat)), + ] + + if speechGender != .neuter { + params.append(URLQueryItem(name: "gender", value: String(describing: speechGender))) + } + + return params + } +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechSynthesizing.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechSynthesizing.swift new file mode 100644 index 000000000..562ab83e7 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechSynthesizing.swift @@ -0,0 +1,91 @@ +import Combine +import Foundation +import MapboxDirections + +/// Protocol for implementing speech synthesizer to be used in ``RouteVoiceController``. +@MainActor +public protocol SpeechSynthesizing: AnyObject, Sendable { + var voiceInstructions: AnyPublisher { get } + + /// Controls muting playback of the synthesizer + var muted: Bool { get set } + /// Controls volume of the voice of the synthesizer. + var volume: VolumeMode { get set } + /// Returns `true` if synthesizer is speaking + var isSpeaking: Bool { get } + /// Locale setting to vocalization. This locale will be used as 'default' if no specific locale is passed for + /// vocalizing each individual instruction. + var locale: Locale? { get set } + /// Controls if this speech synthesizer is allowed to manage the shared `AVAudioSession`. + /// Set this field to `false` if you want to manage the session yourself, for example if your app has background + /// music. + /// Default value is `true`. + var managesAudioSession: Bool { get set } + + /// Used to notify speech synthesizer about future spoken instructions in order to give extra time for preparations. + /// - parameter instructions: An array of ``SpokenInstruction``s that will be encountered further. + /// - parameter locale: A locale to be used for preparing instructions. If `nil` is passed - + /// ``SpeechSynthesizing/locale`` will be used as 'default'. + /// + /// It is not guaranteed that all these instructions will be spoken. For example navigation may be re-routed. + /// This method may be (and most likely will be) called multiple times along the route progress + func prepareIncomingSpokenInstructions(_ instructions: [SpokenInstruction], locale: Locale?) + + /// A request to vocalize the instruction + /// - parameter instruction: an instruction to be vocalized + /// - parameter legProgress: current leg progress, corresponding to the instruction + /// - parameter locale: A locale to be used for vocalizing the instruction. If `nil` is passed - + /// ``SpeechSynthesizing/locale`` will be used as 'default'. + /// + /// This method is not guaranteed to be synchronous or asynchronous. When vocalizing is finished, + /// ``VoiceInstructionEvents/DidSpeak`` should be published by ``voiceInstructions``. + func speak(_ instruction: SpokenInstruction, during legProgress: RouteLegProgress, locale: Locale?) + + /// Tells synthesizer to stop current vocalization in a graceful manner. + func stopSpeaking() + /// Tells synthesizer to stop current vocalization immediately. + func interruptSpeaking() +} + +public protocol VoiceInstructionEvent {} + +public enum VoiceInstructionEvents { + public struct WillSpeak: VoiceInstructionEvent, Equatable { + public let instruction: SpokenInstruction + + public init(instruction: SpokenInstruction) { + self.instruction = instruction + } + } + + public struct DidSpeak: VoiceInstructionEvent, Equatable { + public let instruction: SpokenInstruction + + public init(instruction: SpokenInstruction) { + self.instruction = instruction + } + } + + public struct DidInterrupt: VoiceInstructionEvent, Equatable { + public let interruptedInstruction: SpokenInstruction + public let interruptingInstruction: SpokenInstruction + + public init(interruptedInstruction: SpokenInstruction, interruptingInstruction: SpokenInstruction) { + self.interruptedInstruction = interruptedInstruction + self.interruptingInstruction = interruptingInstruction + } + } + + public struct EncounteredError: VoiceInstructionEvent { + public let error: SpeechError + + public init(error: SpeechError) { + self.error = error + } + } +} + +public enum VolumeMode: Equatable, Sendable { + case system + case override(Float) +} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SystemSpeechSynthesizer.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SystemSpeechSynthesizer.swift new file mode 100644 index 000000000..51b5e7d74 --- /dev/null +++ b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SystemSpeechSynthesizer.swift @@ -0,0 +1,251 @@ +import AVFoundation +import Combine +import MapboxDirections + +/// ``SpeechSynthesizing`` implementation, using ``AVSpeechSynthesizer``. +@_spi(MapboxInternal) +public final class SystemSpeechSynthesizer: NSObject, SpeechSynthesizing { + private let _voiceInstructions: PassthroughSubject = .init() + public var voiceInstructions: AnyPublisher { + _voiceInstructions.eraseToAnyPublisher() + } + + // MARK: Speech Configuration + + public var muted: Bool = false { + didSet { + if isSpeaking { + interruptSpeaking() + } + } + } + + public var volume: VolumeMode { + get { + .system + } + set { + // Do Nothing + // AVSpeechSynthesizer uses 'AVAudioSession.sharedInstance().outputVolume' by default + } + } + + public var locale: Locale? = Locale.autoupdatingCurrent + + /// Controls if this speech synthesizer is allowed to manage the shared `AVAudioSession`. + /// Set this field to `false` if you want to manage the session yourself, for example if your app has background + /// music. + /// Default value is `true`. + public var managesAudioSession = true + + // MARK: Speaking Instructions + + public var isSpeaking: Bool { return speechSynthesizer.isSpeaking } + + private var speechSynthesizer: AVSpeechSynthesizer { + _speechSynthesizer.speechSynthesizer + } + + /// Holds `AVSpeechSynthesizer` that can be sent between isolation contexts but should be operated on MainActor. + /// + /// Motivation: + /// We must stop synthesizer when the instance is deallocated, but deinit isn't guaranteed to be called on + /// MainActor. So we can't safely access synthesizer from it. + private var _speechSynthesizer: SendableSpeechSynthesizer + + private var previousInstruction: SpokenInstruction? + + override public init() { + self._speechSynthesizer = .init(AVSpeechSynthesizer()) + super.init() + speechSynthesizer.delegate = self + } + + deinit { + Task { @MainActor [_speechSynthesizer] in + _speechSynthesizer.speechSynthesizer.stopSpeaking(at: .immediate) + } + } + + public func prepareIncomingSpokenInstructions(_ instructions: [SpokenInstruction], locale: Locale?) { + // Do nothing + } + + public func speak(_ instruction: SpokenInstruction, during legProgress: RouteLegProgress, locale: Locale? = nil) { + guard !muted else { + _voiceInstructions.send( + VoiceInstructionEvents.DidSpeak( + instruction: instruction + ) + ) + return + } + + guard let locale = locale ?? self.locale else { + _voiceInstructions.send( + VoiceInstructionEvents.EncounteredError( + error: SpeechError.undefinedSpeechLocale( + instruction: instruction + ) + ) + ) + return + } + + var utterance: AVSpeechUtterance? + let localeCode = [locale.languageCode, locale.regionCode].compactMap { $0 }.joined(separator: "-") + + if localeCode == "en-US" { + // Alex can’t handle attributed text. + utterance = AVSpeechUtterance(string: instruction.text) + utterance!.voice = AVSpeechSynthesisVoice(identifier: AVSpeechSynthesisVoiceIdentifierAlex) + } + + _voiceInstructions.send(VoiceInstructionEvents.WillSpeak(instruction: instruction)) + + if utterance?.voice == nil { + utterance = AVSpeechUtterance(attributedString: instruction.attributedText(for: legProgress)) + } + + // Only localized languages will have a proper fallback voice + if utterance?.voice == nil { + utterance?.voice = AVSpeechSynthesisVoice(language: localeCode) + } + + guard let utteranceToSpeak = utterance else { + _voiceInstructions.send( + VoiceInstructionEvents.EncounteredError( + error: SpeechError.unsupportedLocale( + locale: Locale.nationalizedCurrent + ) + ) + ) + return + } + if let previousInstruction, speechSynthesizer.isSpeaking { + _voiceInstructions.send( + VoiceInstructionEvents.DidInterrupt( + interruptedInstruction: previousInstruction, + interruptingInstruction: instruction + ) + ) + } + + previousInstruction = instruction + speechSynthesizer.speak(utteranceToSpeak) + } + + public func stopSpeaking() { + speechSynthesizer.stopSpeaking(at: .word) + } + + public func interruptSpeaking() { + speechSynthesizer.stopSpeaking(at: .immediate) + } + + private func safeDuckAudio() { + guard managesAudioSession else { return } + if let error = AVAudioSession.sharedInstance().tryDuckAudio() { + guard let instruction = previousInstruction else { + assertionFailure("Speech Synthesizer finished speaking 'nil' instruction") + return + } + + _voiceInstructions.send( + VoiceInstructionEvents.EncounteredError( + error: SpeechError.unableToControlAudio( + instruction: instruction, + action: .duck, + underlying: error + ) + ) + ) + } + } + + private func safeUnduckAudio() { + guard managesAudioSession else { return } + if let error = AVAudioSession.sharedInstance().tryUnduckAudio() { + guard let instruction = previousInstruction else { + assertionFailure("Speech Synthesizer finished speaking 'nil' instruction") + return + } + + _voiceInstructions.send( + VoiceInstructionEvents.EncounteredError( + error: SpeechError.unableToControlAudio( + instruction: instruction, + action: .unduck, + underlying: error + ) + ) + ) + } + } +} + +extension SystemSpeechSynthesizer: AVSpeechSynthesizerDelegate { + public nonisolated func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, + didStart utterance: AVSpeechUtterance + ) { + MainActor.assumingIsolated { + safeDuckAudio() + } + } + + public nonisolated func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, + didContinue utterance: AVSpeechUtterance + ) { + MainActor.assumingIsolated { + safeDuckAudio() + } + } + + public nonisolated func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, + didFinish utterance: AVSpeechUtterance + ) { + MainActor.assumingIsolated { + safeUnduckAudio() + guard let instruction = previousInstruction else { + assertionFailure("Speech Synthesizer finished speaking 'nil' instruction") + return + } + _voiceInstructions.send(VoiceInstructionEvents.DidSpeak(instruction: instruction)) + } + } + + public nonisolated func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, + didPause utterance: AVSpeechUtterance + ) { + MainActor.assumingIsolated { + safeUnduckAudio() + } + } + + public nonisolated func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, + didCancel utterance: AVSpeechUtterance + ) { + MainActor.assumingIsolated { + safeUnduckAudio() + guard let instruction = previousInstruction else { + assertionFailure("Speech Synthesizer finished speaking 'nil' instruction") + return + } + _voiceInstructions.send(VoiceInstructionEvents.DidSpeak(instruction: instruction)) + } + } +} + +@MainActor +private final class SendableSpeechSynthesizer: Sendable { + let speechSynthesizer: AVSpeechSynthesizer + + init(_ speechSynthesizer: AVSpeechSynthesizer) { + self.speechSynthesizer = speechSynthesizer + } +} 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 From 63de19b543711ce9b40883a5813e9bddf9bae9da Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sun, 26 Jan 2025 14:16:24 +0100 Subject: [PATCH 13/33] fixed pod file --- example/ios/Podfile | 6 +- example/ios/Podfile.lock | 48 +- ios/Classes/Navigation/.gitkeep | 0 .../AdministrativeRegion.swift | 43 - .../Navigation/MapboxDirections/Amenity.swift | 50 - .../MapboxDirections/AmenityType.swift | 61 - .../MapboxDirections/AttributeOptions.swift | 154 -- .../MapboxDirections/BlockedLanes.swift | 134 -- .../MapboxDirections/Congestion.swift | 33 - .../MapboxDirections/Credentials.swift | 80 - .../CustomValueOptionSet.swift | 675 -------- .../MapboxDirections/Directions.swift | 711 --------- .../MapboxDirections/DirectionsError.swift | 215 --- .../MapboxDirections/DirectionsOptions.swift | 610 ------- .../MapboxDirections/DirectionsResult.swift | 245 --- .../MapboxDirections/DrivingSide.swift | 12 - .../MapboxDirections/Extensions/Array.swift | 8 - .../MapboxDirections/Extensions/Codable.swift | 84 - .../Extensions/CoreLocation.swift | 31 - .../MapboxDirections/Extensions/Double.swift | 5 - .../Extensions/ForeignMemberContainer.swift | 117 -- .../MapboxDirections/Extensions/GeoJSON.swift | 58 - .../Extensions/HTTPURLResponse.swift | 30 - .../Extensions/Measurement.swift | 100 -- .../MapboxDirections/Extensions/String.swift | 7 - .../Extensions/URL+Request.swift | 89 -- .../MapboxDirections/Incident.swift | 304 ---- .../MapboxDirections/Interchange.swift | 14 - .../MapboxDirections/Intersection.swift | 483 ------ .../MapboxDirections/IsochroneError.swift | 61 - .../MapboxDirections/IsochroneOptions.swift | 272 ---- .../MapboxDirections/Isochrones.swift | 168 -- .../MapboxDirections/Junction.swift | 14 - .../Navigation/MapboxDirections/Lane.swift | 57 - .../MapboxDirections/LaneIndication.swift | 134 -- .../MapMatching/MapMatchingResponse.swift | 87 - .../MapboxDirections/MapMatching/Match.swift | 163 -- .../MapMatching/MatchOptions.swift | 156 -- .../MapMatching/Tracepoint.swift | 78 - .../MapboxDirections/MapboxDirections.h | 8 - .../MapboxStreetsRoadClass.swift | 56 - .../Navigation/MapboxDirections/Matrix.swift | 166 -- .../MapboxDirections/MatrixError.swift | 63 - .../MapboxDirections/MatrixOptions.swift | 204 --- .../MapboxDirections/MatrixResponse.swift | 128 -- .../MapboxDirections/OfflineDirections.swift | 134 -- .../MapboxDirections/Polyline.swift | 397 ----- .../MapboxDirections/ProfileIdentifier.swift | 48 - .../MapboxDirections/QuickLook.swift | 54 - .../MapboxDirections/RefreshedRoute.swift | 65 - .../ResponseDisposition.swift | 11 - .../MapboxDirections/RestStop.swift | 74 - .../RoadClassExclusionViolation.swift | 16 - .../MapboxDirections/RoadClasses.swift | 175 -- .../Navigation/MapboxDirections/Route.swift | 130 -- .../MapboxDirections/RouteLeg.swift | 549 ------- .../MapboxDirections/RouteLegAttributes.swift | 147 -- .../MapboxDirections/RouteOptions.swift | 657 -------- .../RouteRefreshResponse.swift | 88 - .../MapboxDirections/RouteRefreshSource.swift | 63 - .../MapboxDirections/RouteResponse.swift | 393 ----- .../MapboxDirections/RouteStep.swift | 1112 ------------- .../MapboxDirections/SilentWaypoint.swift | 48 - .../MapboxDirections/SpokenInstruction.swift | 83 - .../MapboxDirections/TollCollection.swift | 52 - .../MapboxDirections/TollPrice.swift | 147 -- .../MapboxDirections/TrafficTendency.swift | 20 - .../MapboxDirections/VisualInstruction.swift | 95 -- .../VisualInstructionBanner.swift | 112 -- .../VisualInstructionComponent.swift | 345 ---- .../MapboxDirections/Waypoint.swift | 349 ---- .../Billing/ApiConfiguration.swift | 78 - .../BillingHandler+SkuTokenProvider.swift | 9 - .../Billing/BillingHandler.swift | 477 ------ .../Billing/SkuTokenProvider.swift | 7 - .../Cache/FileCache.swift | 92 -- .../Cache/SyncBimodalCache.swift | 85 - .../MapboxNavigationCore/CoreConstants.swift | 190 --- .../MapboxNavigationCore/Environment.swift | 13 - .../Extensions/AVAudioSession.swift | 27 - .../Extensions/AmenityType.swift | 51 - .../Extensions/Array++.swift | 37 - .../Extensions/BoundingBox++.swift | 15 - .../Extensions/Bundle.swift | 63 - .../Extensions/CLLocationDirection++.swift | 9 - .../Extensions/CongestionLevel.swift | 118 -- .../Extensions/Coordinate2D.swift | 15 - .../Extensions/Date.swift | 20 - .../Extensions/Dictionary.swift | 13 - .../Extensions/FixLocation.swift | 39 - .../Extensions/Geometry.swift | 64 - .../Extensions/Incident.swift | 80 - .../Extensions/Locale.swift | 47 - .../Extensions/MapboxStreetsRoadClass.swift | 34 - .../Extensions/MeasurementSystem.swift | 10 - .../Extensions/NavigationStatus.swift | 36 - .../Extensions/Preconcurrency+Sendable.swift | 7 - .../Extensions/RestStop.swift | 25 - .../Extensions/Result.swift | 20 - .../Extensions/RouteLeg.swift | 46 - .../Extensions/RouteOptions.swift | 62 - .../Extensions/SpokenInstruction.swift | 52 - .../Extensions/String.swift | 58 - .../Extensions/TollCollection.swift | 18 - .../Extensions/UIDevice.swift | 22 - .../Extensions/UIEdgeInsets.swift | 33 - .../Extensions/Utils.swift | 57 - .../ActiveNavigationFeedbackType.swift | 79 - .../Feedback/EventFixLocation.swift | 119 -- .../Feedback/EventStep.swift | 55 - .../Feedback/EventsManager.swift | 177 --- .../Feedback/FeedbackEvent.swift | 16 - .../Feedback/FeedbackMetadata.swift | 103 -- .../Feedback/FeedbackScreenshotOption.swift | 7 - .../Feedback/FeedbackType.swift | 7 - .../NavigationEventsManagerError.swift | 8 - .../PassiveNavigationFeedbackType.swift | 36 - .../Feedback/SearchFeedbackType.swift | 35 - .../History/Copilot/AttachmentsUploader.swift | 152 -- .../History/Copilot/CopilotService.swift | 99 -- .../Copilot/Events/ApplicationState.swift | 21 - .../History/Copilot/Events/DriveEnds.swift | 20 - .../History/Copilot/Events/InitRoute.swift | 30 - .../Copilot/Events/NavigationFeedback.swift | 15 - .../Events/NavigationHistoryEvent.swift | 12 - .../Copilot/Events/SearchResultUsed.swift | 62 - .../Copilot/Events/SearchResults.swift | 60 - .../Copilot/FeedbackEventsObserver.swift | 99 -- .../History/Copilot/MapboxCopilot.swift | 207 --- .../Copilot/MapboxCopilotDelegate.swift | 14 - .../NavigationHistoryAttachmentProvider.swift | 132 -- .../NavigationHistoryErrorReport.swift | 35 - .../NavigationHistoryEventsController.swift | 125 -- .../Copilot/NavigationHistoryFormat.swift | 47 - .../NavigationHistoryLocalStorage.swift | 78 - .../Copilot/NavigationHistoryManager.swift | 115 -- .../Copilot/NavigationHistoryProvider.swift | 15 - .../Copilot/NavigationHistoryUploader.swift | 70 - .../History/Copilot/NavigationSession.swift | 75 - .../Copilot/Utils/AppEnvironment.swift | 28 - .../History/Copilot/Utils/FileManager++.swift | 27 - .../Copilot/Utils/TokenOwnerProvider.swift | 33 - .../History/Copilot/Utils/TypeConverter.swift | 27 - .../History/HistoryEvent.swift | 82 - .../History/HistoryReader.swift | 198 --- .../History/HistoryRecorder.swift | 46 - .../History/HistoryRecording.swift | 73 - .../History/HistoryReplayer.swift | 351 ---- .../IdleTimerManager.swift | 84 - .../Localization/LocalizationManager.swift | 48 - .../Localization/String+Localization.swift | 18 - .../Map/Camera/CameraStateTransition.swift | 34 - .../Map/Camera/FollowingCameraOptions.swift | 260 --- .../Map/Camera/NavigationCamera.swift | 243 --- .../Camera/NavigationCameraDebugView.swift | 199 --- .../Map/Camera/NavigationCameraOptions.swift | 22 - .../Map/Camera/NavigationCameraState.swift | 24 - .../NavigationCameraStateTransition.swift | 209 --- .../Map/Camera/NavigationCameraType.swift | 8 - .../NavigationViewportDataSourceOptions.swift | 36 - .../Map/Camera/OverviewCameraOptions.swift | 78 - .../CarPlayViewportDataSource.swift | 312 ---- .../CommonViewportDataSource.swift | 80 - .../MobileViewportDataSource.swift | 341 ---- .../ViewportDataSource+Calculation.swift | 146 -- .../ViewportDataSource.swift | 58 - .../ViewportDataSourceState.swift | 28 - .../Camera/ViewportParametersProvider.swift | 100 -- .../MapboxNavigationCore/Map/MapPoint.swift | 16 - .../MapboxNavigationCore/Map/MapView.swift | 205 --- ...gationMapView+ContinuousAlternatives.swift | 64 - .../Map/NavigationMapView+Gestures.swift | 206 --- ...NavigationMapView+VanishingRouteLine.swift | 212 --- .../Map/NavigationMapView.swift | 736 --------- .../Map/NavigationMapViewDelegate.swift | 289 ---- .../Map/Other/Array.swift | 159 -- .../Map/Other/CLLocationCoordinate2D++.swift | 28 - .../Map/Other/CongestionSegment.swift | 6 - .../Map/Other/Cosntants.swift | 22 - .../Map/Other/Expression++.swift | 61 - .../Map/Other/Feature++.swift | 28 - .../Map/Other/MapboxMap+Async.swift | 54 - .../Map/Other/NavigationMapIdentifiers.swift | 37 - .../Map/Other/PuckConfigurations.swift | 41 - .../Map/Other/RoadAlertType.swift | 121 -- .../Map/Other/RoadClassesSegment.swift | 6 - .../Map/Other/Route.swift | 217 --- .../RouteDurationAnnotationTailPosition.swift | 6 - .../Map/Other/RoutesPresentationStyle.swift | 14 - .../Map/Other/UIColor++.swift | 40 - .../Map/Other/UIFont.swift | 8 - .../Map/Other/UIImage++.swift | 47 - .../Map/Other/VectorSource++.swift | 84 - .../Style/AlternativeRoute+Deviation.swift | 29 - .../CongestionColorsConfiguration.swift | 76 - .../Congestion/CongestionConfiguration.swift | 24 - .../Map/Style/FeatureIds.swift | 169 -- .../IntersectionAnnotationsMapFeatures.swift | 133 -- .../Map/Style/ManeuverArrowMapFeatures.swift | 131 -- .../Map/Style/MapFeatures/ETAView.swift | 285 ---- .../ETAViewsAnnotationFeature.swift | 139 -- .../Style/MapFeatures/GeoJsonMapFeature.swift | 239 --- .../Map/Style/MapFeatures/MapFeature.swift | 16 - .../Style/MapFeatures/MapFeaturesStore.swift | 117 -- .../Map/Style/MapFeatures/Style++.swift | 38 - .../Map/Style/MapLayersOrder.swift | 260 --- .../Map/Style/NavigationMapStyleManager.swift | 537 ------- .../RouteAlertsAnnotationsMapFeatures.swift | 364 ----- .../Style/RouteAnnotationMapFeatures.swift | 55 - .../Map/Style/RouteLineMapFeatures.swift | 406 ----- .../Style/VoiceInstructionsMapFeatures.swift | 66 - .../Map/Style/WaypointsMapFeature.swift | 156 -- .../ElectronicHorizonController.swift | 20 - .../MapboxNavigation/MapboxNavigation.swift | 47 - .../NavigationController.swift | 60 - .../MapboxNavigation/SessionController.swift | 44 - .../MapboxNavigationProvider.swift | 374 ----- .../Navigator/AlternativeRoute.swift | 175 -- .../Navigator/BorderCrossing.swift | 32 - .../EHorizon/DistancedRoadObject.swift | 155 -- .../EHorizon/ElectronicHorizonConfig.swift | 57 - .../Navigator/EHorizon/Interchange.swift | 32 - .../Navigator/EHorizon/Junction.swift | 32 - .../EHorizon/LocalizedRoadObjectName.swift | 24 - .../Navigator/EHorizon/OpenLRIdentifier.swift | 32 - .../EHorizon/OpenLROrientation.swift | 38 - .../Navigator/EHorizon/OpenLRSideOfRoad.swift | 38 - .../Navigator/EHorizon/RoadGraph.swift | 70 - .../Navigator/EHorizon/RoadGraphEdge.swift | 73 - .../EHorizon/RoadGraphEdgeMetadata.swift | 195 --- .../Navigator/EHorizon/RoadGraphPath.swift | 66 - .../EHorizon/RoadGraphPosition.swift | 42 - .../Navigator/EHorizon/RoadName.swift | 42 - .../Navigator/EHorizon/RoadObject.swift | 78 - .../EHorizon/RoadObjectEdgeLocation.swift | 38 - .../Navigator/EHorizon/RoadObjectKind.swift | 124 -- .../EHorizon/RoadObjectLocation.swift | 127 -- .../EHorizon/RoadObjectMatcher.swift | 215 --- .../EHorizon/RoadObjectMatcherDelegate.swift | 29 - .../EHorizon/RoadObjectMatcherError.swift | 36 - .../EHorizon/RoadObjectPosition.swift | 29 - .../Navigator/EHorizon/RoadObjectStore.swift | 126 -- .../EHorizon/RoadObjectStoreDelegate.swift | 21 - .../Navigator/EHorizon/RoadShield.swift | 42 - .../Navigator/EHorizon/RoadSubgraphEdge.swift | 65 - .../Navigator/EHorizon/RouteAlert.swift | 22 - .../Navigator/EtaDistanceInfo.swift | 12 - .../Navigator/FasterRouteController.swift | 152 -- .../CoreNavigator/CoreNavigator.swift | 518 ------ .../DefaultRerouteControllerInterface.swift | 29 - .../NavigationNativeNavigator.swift | 143 -- .../NavigationSessionManager.swift | 42 - .../NavigatorElectronicHorizonObserver.swift | 58 - .../NavigatorFallbackVersionsObserver.swift | 66 - .../NavigatorRouteAlternativesObserver.swift | 42 - .../NavigatorRouteRefreshObserver.swift | 67 - .../NavigatorStatusObserver.swift | 18 - .../CoreNavigator/RerouteController.swift | 151 -- .../ReroutingControllerDelegate.swift | 51 - .../Navigator/Internals/HandlerFactory.swift | 87 - .../Movement/NavigationMovementMonitor.swift | 84 - .../Internals/NativeHandlersFactory.swift | 289 ---- .../Internals/RoutesCoordinator.swift | 103 -- .../Internals/Simulation/DispatchTimer.swift | 101 -- .../Simulation/SimulatedLocationManager.swift | 493 ------ .../LocationClient/LocationClient.swift | 96 -- .../LocationClient/LocationSource.swift | 8 - .../MultiplexLocationClient.swift | 139 -- .../SimulatedLocationManagerWrapper.swift | 73 - .../Navigator/MapMatchingResult.swift | 79 - .../Navigator/MapboxNavigator.swift | 1414 ----------------- .../Navigator/NavigationLocationManager.swift | 44 - .../Navigator/NavigationRoutes.swift | 420 ----- .../Navigator/Navigator.swift | 403 ----- .../Navigator/RoadInfo.swift | 40 - .../RouteProgress/RouteLegProgress.swift | 231 --- .../RouteProgress/RouteProgress.swift | 419 ----- .../RouteProgress/RouteStepProgress.swift | 277 ---- .../Navigator/SpeedLimit.swift | 7 - .../Navigator/Tunnel.swift | 13 - .../PredictiveCacheConfig.swift | 29 - .../PredictiveCacheLocationConfig.swift | 39 - .../PredictiveCacheManager.swift | 111 -- .../PredictiveCacheMapsConfig.swift | 31 - .../PredictiveCacheNavigationConfig.swift | 13 - .../PredictiveCacheSearchConfig.swift | 25 - .../MapboxNavigationCore/Resources/3DPuck.glb | Bin 31480 -> 0 bytes .../Resources/Assets.xcassets/Contents.json | 6 - .../RoadIntersections/Contents.json | 6 - .../RailroadCrossing.imageset/Contents.json | 12 - .../RailroadCrossing.pdf | Bin 14532 -> 0 bytes .../StopSign.imageset/Contents.json | 16 - .../StopSign.imageset/StopSign.pdf | Bin 19197 -> 0 bytes .../TrafficSignal.imageset/Contents.json | 16 - .../TrafficSignal.imageset/TrafficSignal.pdf | Bin 10550 -> 0 bytes .../YieldSign.imageset/Contents.json | 16 - .../YieldSign.imageset/YieldSign.pdf | Bin 10639 -> 0 bytes .../Assets.xcassets/RouteAlerts/Contents.json | 6 - .../ra_accident.imageset/Contents.json | 12 - .../ra_accident.imageset/ra_accident.pdf | Bin 18167 -> 0 bytes .../ra_congestion.imageset/Contents.json | 12 - .../ra_congestion.imageset/ra_congestion.pdf | Bin 19663 -> 0 bytes .../ra_construction.imageset/Contents.json | 12 - .../ra_contruction.pdf | Bin 15309 -> 0 bytes .../Contents.json | 12 - .../ra_disabled_vehicle.pdf | Bin 16033 -> 0 bytes .../Contents.json | 12 - .../ra_lane_restriction.pdf | Bin 17449 -> 0 bytes .../ra_mass_transit.imageset/Contents.json | 12 - .../ra_mass_transit.pdf | Bin 15442 -> 0 bytes .../ra_miscellaneous.imageset/Contents.json | 12 - .../ra_miscellaneous.pdf | Bin 11988 -> 0 bytes .../ra_other_news.imageset/Contents.json | 12 - .../ra_other_news.imageset/ra_other_news.pdf | Bin 12005 -> 0 bytes .../ra_planned_event.imageset/Contents.json | 12 - .../ra_planned_event.pdf | Bin 13986 -> 0 bytes .../ra_road_closure.imageset/Contents.json | 12 - .../ra_road_closure.pdf | Bin 11476 -> 0 bytes .../ra_road_hazard.imageset/Contents.json | 12 - .../ra_road_hazard.pdf | Bin 12005 -> 0 bytes .../ra_weather.imageset/Contents.json | 12 - .../ra_weather.imageset/ra_weather.pdf | Bin 17306 -> 0 bytes .../midpoint_marker.imageset/Contents.json | 12 - .../midpoint_marker.pdf | Bin 21017 -> 0 bytes .../puck.imageset/Contents.json | 12 - .../Assets.xcassets/puck.imageset/puck.pdf | Bin 19977 -> 0 bytes .../triangle.imageset/Contents.json | 15 - .../triangle.imageset/triangle.pdf | Bin 3952 -> 0 bytes .../Resources/Sounds/reroute-sound.pcm | Bin 66284 -> 0 bytes .../Resources/ar.lproj/Localizable.strings | 2 - .../Resources/bg.lproj/Localizable.strings | 2 - .../Resources/ca.lproj/Localizable.strings | 8 - .../Resources/cs.lproj/Localizable.strings | 2 - .../Resources/da.lproj/Localizable.strings | 2 - .../Resources/de.lproj/Localizable.strings | 8 - .../Resources/el.lproj/Localizable.strings | 2 - .../Resources/en.lproj/Localizable.strings | 8 - .../Resources/es.lproj/Localizable.strings | 8 - .../Resources/et.lproj/Localizable.strings | 2 - .../Resources/fi.lproj/Localizable.strings | 2 - .../Resources/fr.lproj/Localizable.strings | 2 - .../Resources/he.lproj/Localizable.strings | 2 - .../Resources/hr.lproj/Localizable.strings | 2 - .../Resources/hu.lproj/Localizable.strings | 2 - .../Resources/it.lproj/Localizable.strings | 2 - .../Resources/ja.lproj/Localizable.strings | 2 - .../Resources/ms.lproj/Localizable.strings | 2 - .../Resources/nl.lproj/Localizable.strings | 2 - .../Resources/no.lproj/Localizable.strings | 2 - .../Resources/pl.lproj/Localizable.strings | 8 - .../Resources/pt-BR.lproj/Localizable.strings | 2 - .../Resources/pt-PT.lproj/Localizable.strings | 2 - .../Resources/ro.lproj/Localizable.strings | 2 - .../Resources/ru.lproj/Localizable.strings | 8 - .../Resources/sk.lproj/Localizable.strings | 2 - .../Resources/sl.lproj/Localizable.strings | 2 - .../Resources/sr.lproj/Localizable.strings | 2 - .../Resources/sv.lproj/Localizable.strings | 2 - .../Resources/tr.lproj/Localizable.strings | 2 - .../Resources/uk.lproj/Localizable.strings | 8 - .../Resources/vi.lproj/Localizable.strings | 8 - .../zh-Hans.lproj/Localizable.strings | 2 - .../zh-Hant.lproj/Localizable.strings | 2 - .../Routing/MapboxRoutingProvider.swift | 253 --- .../Routing/NavigationRouteOptions.swift | 227 --- .../Routing/RoutingProvider.swift | 45 - .../MapboxNavigationCore/SdkInfo.swift | 23 - .../AlternativeRoutesDetectionConfig.swift | 103 -- .../Settings/BillingHandlerProvider.swift | 16 - .../Settings/Configuration/CoreConfig.swift | 222 --- .../Configuration/UnitOfMeasurement.swift | 11 - .../Settings/CustomRoutingProvider.swift | 16 - .../Settings/EventsManagerProvider.swift | 16 - .../Settings/FasterRouteDetectionConfig.swift | 44 - .../Settings/HistoryRecordingConfig.swift | 16 - .../Settings/IncidentsConfig.swift | 26 - .../NavigationCoreApiConfiguration.swift | 41 - .../Settings/RerouteConfig.swift | 26 - .../Settings/RoutingConfig.swift | 87 - .../Settings/SettingsWrappers.swift | 40 - .../Settings/StatusUpdatingSettings.swift | 22 - .../Settings/TTSConfig.swift | 19 - .../Settings/TelemetryAppMetadata.swift | 48 - .../Settings/TileStoreConfiguration.swift | 65 - .../Telemetry/ConnectivityTypeProvider.swift | 75 - .../Telemetry/EventAppState.swift | 153 -- .../Telemetry/EventsMetadataProvider.swift | 176 -- .../NavigationNativeEventsManager.swift | 174 -- .../NavigationTelemetryManager.swift | 42 - .../MapboxNavigationCore/Typealiases.swift | 32 - .../Utils/NavigationLog.swift | 58 - .../Utils/ScreenCapture.swift | 47 - .../Utils/UnimplementedLogging.swift | 98 -- .../Utils/UserAgent.swift | 100 -- .../MapboxNavigationCore/Version.swift | 6 - .../VoiceGuidance/AudioPlayerClient.swift | 93 -- .../VoiceGuidance/AudioPlayerDelegate.swift | 11 - .../MapboxSpeechSynthesizer.swift | 381 ----- .../MultiplexedSpeechSynthesizer.swift | 181 --- .../VoiceGuidance/RouteVoiceController.swift | 150 -- .../VoiceGuidance/Speech.swift | 201 --- .../VoiceGuidance/SpeechError.swift | 60 - .../VoiceGuidance/SpeechOptions.swift | 82 - .../VoiceGuidance/SpeechSynthesizing.swift | 91 -- .../SystemSpeechSynthesizer.swift | 251 --- ios/MapboxDirections.podspec | 28 - ios/MapboxNavigationCore.podspec | 33 - ios/Turf.podspec | 45 - ios/mapbox_maps_flutter.podspec | 61 +- mapbox-maps-flutter | 1 + 410 files changed, 26 insertions(+), 38244 deletions(-) delete mode 100644 ios/Classes/Navigation/.gitkeep delete mode 100644 ios/Classes/Navigation/MapboxDirections/AdministrativeRegion.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Amenity.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/AmenityType.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/AttributeOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/BlockedLanes.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Congestion.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Credentials.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/CustomValueOptionSet.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Directions.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/DirectionsError.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/DirectionsOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/DirectionsResult.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/DrivingSide.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/Array.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/Codable.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/CoreLocation.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/Double.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/ForeignMemberContainer.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/GeoJSON.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/HTTPURLResponse.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/Measurement.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/String.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Extensions/URL+Request.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Incident.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Interchange.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Intersection.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/IsochroneError.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/IsochroneOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Isochrones.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Junction.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Lane.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/LaneIndication.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/MapMatching/MapMatchingResponse.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/MapMatching/Match.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/MapMatching/MatchOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/MapMatching/Tracepoint.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/MapboxDirections.h delete mode 100644 ios/Classes/Navigation/MapboxDirections/MapboxStreetsRoadClass.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Matrix.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/MatrixError.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/MatrixOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/MatrixResponse.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/OfflineDirections.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Polyline.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/ProfileIdentifier.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/QuickLook.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RefreshedRoute.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/ResponseDisposition.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RestStop.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RoadClassExclusionViolation.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RoadClasses.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Route.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RouteLeg.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RouteLegAttributes.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RouteOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RouteRefreshResponse.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RouteRefreshSource.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RouteResponse.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/RouteStep.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/SilentWaypoint.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/SpokenInstruction.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/TollCollection.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/TollPrice.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/TrafficTendency.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/VisualInstruction.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/VisualInstructionBanner.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/VisualInstructionComponent.swift delete mode 100644 ios/Classes/Navigation/MapboxDirections/Waypoint.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Billing/ApiConfiguration.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler+SkuTokenProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Billing/SkuTokenProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Cache/FileCache.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Cache/SyncBimodalCache.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/CoreConstants.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Environment.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/AVAudioSession.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/AmenityType.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Array++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/BoundingBox++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Bundle.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/CLLocationDirection++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/CongestionLevel.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Coordinate2D.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Date.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Dictionary.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/FixLocation.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Geometry.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Incident.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Locale.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/MapboxStreetsRoadClass.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/MeasurementSystem.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/NavigationStatus.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Preconcurrency+Sendable.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/RestStop.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Result.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteLeg.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/SpokenInstruction.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/String.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/TollCollection.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIDevice.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIEdgeInsets.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Extensions/Utils.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/ActiveNavigationFeedbackType.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventFixLocation.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventStep.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventsManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackEvent.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackMetadata.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackScreenshotOption.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackType.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/NavigationEventsManagerError.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/PassiveNavigationFeedbackType.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Feedback/SearchFeedbackType.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/AttachmentsUploader.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/CopilotService.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/ApplicationState.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/DriveEnds.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/InitRoute.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationFeedback.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationHistoryEvent.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResultUsed.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResults.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/FeedbackEventsObserver.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilot.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilotDelegate.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryAttachmentProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryErrorReport.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryEventsController.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryFormat.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryLocalStorage.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryUploader.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationSession.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/AppEnvironment.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/FileManager++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TokenOwnerProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TypeConverter.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/HistoryEvent.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReader.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecorder.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecording.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReplayer.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/IdleTimerManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Localization/LocalizationManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Localization/String+Localization.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/CameraStateTransition.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/FollowingCameraOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCamera.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraDebugView.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraState.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraStateTransition.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraType.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationViewportDataSourceOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/OverviewCameraOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CarPlayViewportDataSource.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CommonViewportDataSource.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/MobileViewportDataSource.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource+Calculation.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSourceState.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportParametersProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/MapPoint.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/MapView.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+ContinuousAlternatives.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+Gestures.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+VanishingRouteLine.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapViewDelegate.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Array.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/CLLocationCoordinate2D++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/CongestionSegment.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Cosntants.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Expression++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Feature++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/MapboxMap+Async.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/PuckConfigurations.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadAlertType.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadClassesSegment.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Route.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RouteDurationAnnotationTailPosition.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoutesPresentationStyle.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIColor++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIFont.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIImage++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Other/VectorSource++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionColorsConfiguration.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionConfiguration.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/FeatureIds.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapLayersOrder.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/ElectronicHorizonController.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/MapboxNavigation.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/NavigationController.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/SessionController.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigationProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/AlternativeRoute.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/BorderCrossing.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/DistancedRoadObject.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/ElectronicHorizonConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Interchange.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Junction.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/LocalizedRoadObjectName.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRIdentifier.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLROrientation.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRSideOfRoad.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraph.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdge.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdgeMetadata.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPath.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPosition.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadName.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObject.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectEdgeLocation.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectKind.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectLocation.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcher.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherDelegate.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherError.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectPosition.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStore.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStoreDelegate.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadShield.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadSubgraphEdge.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RouteAlert.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/EtaDistanceInfo.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/FasterRouteController.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/CoreNavigator.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/DefaultRerouteControllerInterface.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationNativeNavigator.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationSessionManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorElectronicHorizonObserver.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorFallbackVersionsObserver.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteAlternativesObserver.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteRefreshObserver.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorStatusObserver.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/RerouteController.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/ReroutingControllerDelegate.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/HandlerFactory.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Movement/NavigationMovementMonitor.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/NativeHandlersFactory.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/RoutesCoordinator.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/DispatchTimer.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/SimulatedLocationManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationClient.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationSource.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/MultiplexLocationClient.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/SimulatedLocationManagerWrapper.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapMatchingResult.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapboxNavigator.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationLocationManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationRoutes.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Navigator.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/RoadInfo.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteLegProgress.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteProgress.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteStepProgress.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/SpeedLimit.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Navigator/Tunnel.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheLocationConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheMapsConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheNavigationConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheSearchConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/3DPuck.glb delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/RailroadCrossing.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/StopSign.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/StopSign.imageset/StopSign.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/TrafficSignal.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/TrafficSignal.imageset/TrafficSignal.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/YieldSign.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/ra_accident.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_congestion.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_congestion.imageset/ra_congestion.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/ra_contruction.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/ra_disabled_vehicle.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/ra_lane_restriction.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/ra_mass_transit.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/ra_miscellaneous.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/ra_other_news.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/ra_planned_event.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/ra_road_closure.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_hazard.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_hazard.imageset/ra_road_hazard.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/ra_weather.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/midpoint_marker.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/puck.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/triangle.imageset/Contents.json delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/triangle.imageset/triangle.pdf delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/Sounds/reroute-sound.pcm delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ar.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/bg.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ca.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/cs.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/da.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/de.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/el.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/en.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/es.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/et.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/fi.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/fr.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/he.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/hr.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/hu.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/it.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ja.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ms.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/nl.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/no.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/pl.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-BR.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-PT.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ro.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/ru.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/sk.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/sl.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/sr.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/sv.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/tr.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/uk.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/vi.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hans.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hant.lproj/Localizable.strings delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Routing/MapboxRoutingProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Routing/NavigationRouteOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Routing/RoutingProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/SdkInfo.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/AlternativeRoutesDetectionConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/BillingHandlerProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/CoreConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/UnitOfMeasurement.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/CustomRoutingProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/EventsManagerProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/FasterRouteDetectionConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/HistoryRecordingConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/IncidentsConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/NavigationCoreApiConfiguration.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/RerouteConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/RoutingConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/SettingsWrappers.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/StatusUpdatingSettings.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/TTSConfig.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/TelemetryAppMetadata.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Settings/TileStoreConfiguration.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Telemetry/ConnectivityTypeProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventAppState.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventsMetadataProvider.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationNativeEventsManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationTelemetryManager.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Typealiases.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Utils/NavigationLog.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Utils/ScreenCapture.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Utils/UnimplementedLogging.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Utils/UserAgent.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/Version.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerClient.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerDelegate.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MapboxSpeechSynthesizer.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MultiplexedSpeechSynthesizer.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/RouteVoiceController.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/Speech.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechError.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechOptions.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechSynthesizing.swift delete mode 100644 ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SystemSpeechSynthesizer.swift delete mode 100644 ios/MapboxDirections.podspec delete mode 100644 ios/MapboxNavigationCore.podspec delete mode 100644 ios/Turf.podspec create mode 160000 mapbox-maps-flutter diff --git a/example/ios/Podfile b/example/ios/Podfile index 900f0959b..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, '13.0' +platform :ios, '14.0' use_frameworks! # CocoaPods analytics sends network stats synchronously affecting flutter build latency. @@ -44,7 +44,3 @@ post_install do |installer| flutter_additional_ios_build_settings(target) end end - -pod 'Turf', :podspec => '../../ios/Turf.podspec' -pod 'MapboxDirections', :podspec => '../../ios/MapboxDirections.podspec' -pod 'MapboxNavigationCore', :podspec => '../../ios/MapboxNavigationCore.podspec', :modular_headers => false diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 8f5c30fa1..9d4743cc8 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -3,29 +3,27 @@ PODS: - integration_test (0.0.1): - Flutter - mapbox_maps_flutter (2.4.0): - - Flutter - - mapbox_maps_flutter/MapboxDirections (= 2.4.0) - - mapbox_maps_flutter/MapboxNavigationCore (= 2.4.0) - - MapboxMaps (= 11.8.0) - - Turf (= 3.0.0) - - mapbox_maps_flutter/MapboxDirections (2.4.0): - - Flutter - - MapboxMaps (= 11.8.0) - - Turf (= 3.0.0) - - mapbox_maps_flutter/MapboxNavigationCore (2.4.0): - Flutter - MapboxMaps (= 11.8.0) + - MapboxNavigationCoreUnofficial (= 3.5.0) - Turf (= 3.0.0) - MapboxCommon (24.8.0) - MapboxCoreMaps (11.8.0): - MapboxCommon (~> 24.8) - - MapboxDirections (3.5.0): + - MapboxDirectionsUnofficial (3.5.0): - Turf (= 3.0.0) - MapboxMaps (11.8.0): - MapboxCommon (= 24.8.0) - MapboxCoreMaps (= 11.8.0) - Turf (= 3.0.0) - - MapboxNavigationCore (3.5.0) + - MapboxNavigationCoreUnofficial (3.5.0): + - 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 @@ -37,17 +35,19 @@ DEPENDENCIES: - Flutter (from `Flutter`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - mapbox_maps_flutter (from `.symlinks/plugins/mapbox_maps_flutter/ios`) - - MapboxDirections (from `../../ios/MapboxDirections.podspec`) - - MapboxNavigationCore (from `../../ios/MapboxNavigationCore.podspec`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - Turf (from `../../ios/Turf.podspec`) SPEC REPOS: trunk: - MapboxCommon - MapboxCoreMaps + - MapboxDirectionsUnofficial - MapboxMaps + - MapboxNavigationCoreUnofficial + - MapboxNavigationHelpersUnofficial + - MapboxNavigationNative + - Turf EXTERNAL SOURCES: Flutter: @@ -56,30 +56,26 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" mapbox_maps_flutter: :path: ".symlinks/plugins/mapbox_maps_flutter/ios" - MapboxDirections: - :podspec: "../../ios/MapboxDirections.podspec" - MapboxNavigationCore: - :podspec: "../../ios/MapboxNavigationCore.podspec" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - Turf: - :podspec: "../../ios/Turf.podspec" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - mapbox_maps_flutter: 4966dceac0f20eb390b78ac328bfd6fbe7585021 + mapbox_maps_flutter: c32deef4a666ff7a7581eba0becf24a5c9381ed9 MapboxCommon: 95fe03b74d0d0ca39dc646ca14862deb06875151 MapboxCoreMaps: f2a82182c5f6c6262220b81547c6df708012932b - MapboxDirections: 241eac54a7ec44425e80b1a06d5a24db111014cb + MapboxDirectionsUnofficial: 4244d39727c60672e45800784e121782d55a60ad MapboxMaps: dbe1869006c5918d62efc6b475fb884947ea2ecd - MapboxNavigationCore: c256657b4313e10c62f95568029126b65a2261ea + MapboxNavigationCoreUnofficial: ddfd6bd636793d4c4aa6617b19bffaab1354076e + MapboxNavigationHelpersUnofficial: 325ef24b1487c336572dad217e35a41be8199eae + MapboxNavigationNative: 3a300f654f9673c6e4cc5f6743997cc3a4c5ceae path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - Turf: c4e870016295bce16600f33aa398a2776ed5f6e9 + Turf: a1604e74adce15c58462c9ae2acdbf049d5be35e -PODFILE CHECKSUM: 98334460b64066f2bc0ce2995da4713f6a0238e5 +PODFILE CHECKSUM: b3192822a93c0d8045ea3b93674ed183e8ddffc6 COCOAPODS: 1.16.2 diff --git a/ios/Classes/Navigation/.gitkeep b/ios/Classes/Navigation/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Classes/Navigation/MapboxDirections/AdministrativeRegion.swift b/ios/Classes/Navigation/MapboxDirections/AdministrativeRegion.swift deleted file mode 100644 index 6938ae717..000000000 --- a/ios/Classes/Navigation/MapboxDirections/AdministrativeRegion.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import Turf - -/// ``AdministrativeRegion`` describes corresponding object on the route. -/// -/// You can also use ``Intersection/regionCode`` or ``RouteLeg/regionCode(atStepIndex:intersectionIndex:)`` to -/// retrieve ISO 3166-1 country code. -public struct AdministrativeRegion: Codable, Equatable, ForeignMemberContainer, Sendable { - public var foreignMembers: JSONObject = [:] - - private enum CodingKeys: String, CodingKey { - case countryCodeAlpha3 = "iso_3166_1_alpha3" - case countryCode = "iso_3166_1" - } - - /// ISO 3166-1 alpha-3 country code - public var countryCodeAlpha3: String? - /// ISO 3166-1 country code - public var countryCode: String - - public init(countryCode: String, countryCodeAlpha3: String) { - self.countryCode = countryCode - self.countryCodeAlpha3 = countryCodeAlpha3 - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.countryCode = try container.decode(String.self, forKey: .countryCode) - self.countryCodeAlpha3 = try container.decodeIfPresent(String.self, forKey: .countryCodeAlpha3) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(countryCode, forKey: .countryCode) - try container.encodeIfPresent(countryCodeAlpha3, forKey: .countryCodeAlpha3) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Amenity.swift b/ios/Classes/Navigation/MapboxDirections/Amenity.swift deleted file mode 100644 index 37a4ffb6a..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Amenity.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation - -/// Provides information about amenity that is available at a given ``RestStop``. -public struct Amenity: Codable, Equatable, Sendable { - /// Name of the amenity, if available. - public let name: String? - - /// Brand of the amenity, if available. - public let brand: String? - - /// Type of the amenity. - public let type: AmenityType - - private enum CodingKeys: String, CodingKey { - case type - case name - case brand - } - - /// Initializes an ``Amenity``. - /// - Parameters: - /// - type: Type of the amenity. - /// - name: Name of the amenity. - /// - brand: Brand of the amenity. - public init(type: AmenityType, name: String? = nil, brand: String? = nil) { - self.type = type - self.name = name - self.brand = brand - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.type = try container.decode(AmenityType.self, forKey: .type) - self.name = try container.decodeIfPresent(String.self, forKey: .name) - self.brand = try container.decodeIfPresent(String.self, forKey: .brand) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type, forKey: .type) - try container.encodeIfPresent(name, forKey: .name) - try container.encodeIfPresent(brand, forKey: .brand) - } - - public static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.name == rhs.name && - lhs.brand == rhs.brand && - lhs.type == rhs.type - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/AmenityType.swift b/ios/Classes/Navigation/MapboxDirections/AmenityType.swift deleted file mode 100644 index 73889ccf7..000000000 --- a/ios/Classes/Navigation/MapboxDirections/AmenityType.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation - -/// Type of the ``Amenity``. -public enum AmenityType: String, Codable, Equatable, Sendable { - /// Undefined amenity type. - case undefined - - /// Gas station amenity type. - case gasStation = "gas_station" - - /// Electric charging station amenity type. - case electricChargingStation = "electric_charging_station" - - /// Toilet amenity type. - case toilet - - /// Coffee amenity type. - case coffee - - /// Restaurant amenity type. - case restaurant - - /// Snack amenity type. - case snack - - /// ATM amenity type. - case ATM - - /// Info amenity type. - case info - - /// Baby care amenity type. - case babyCare = "baby_care" - - /// Facilities for disabled amenity type. - case facilitiesForDisabled = "facilities_for_disabled" - - /// Shop amenity type. - case shop - - /// Telephone amenity type. - case telephone - - /// Hotel amenity type. - case hotel - - /// Hot spring amenity type. - case hotSpring = "hotspring" - - /// Shower amenity type. - case shower - - /// Picnic shelter amenity type. - case picnicShelter = "picnic_shelter" - - /// Post amenity type. - case post - - /// Fax amenity type. - case fax -} diff --git a/ios/Classes/Navigation/MapboxDirections/AttributeOptions.swift b/ios/Classes/Navigation/MapboxDirections/AttributeOptions.swift deleted file mode 100644 index efab61cf0..000000000 --- a/ios/Classes/Navigation/MapboxDirections/AttributeOptions.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Foundation - -/// Attributes are metadata information for a route leg. -/// -/// When any of the attributes are specified, the resulting route leg contains one attribute value for each segment in -/// leg, where a segment is the straight line between two coordinates in the route leg’s full geometry. -public struct AttributeOptions: CustomValueOptionSet, CustomStringConvertible, Equatable, Sendable { - public var rawValue: Int - - public var customOptionsByRawValue: [Int: String] = [:] - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - public init() { - self.rawValue = 0 - } - - /// Live-traffic closures along the road segment. - /// - /// When this attribute is specified, the ``RouteLeg/closures`` property is filled with relevant data. - /// - /// This attribute requires ``ProfileIdentifier/automobileAvoidingTraffic`` and is supported only by Directions and - /// Map Matching requests. - public static let closures = AttributeOptions(rawValue: 1) - - /// Distance (in meters) along the segment. - /// - /// When this attribute is specified, the ``RouteLeg/segmentDistances`` property contains one value for each segment - /// in the leg’s full geometry. - /// When used in Matrix request - will produce a distances matrix in response. - public static let distance = AttributeOptions(rawValue: 1 << 1) - - /// Expected travel time (in seconds) along the segment. - /// - /// When this attribute is specified, the ``RouteLeg/expectedSegmentTravelTimes`` property contains one value for - /// each segment in the leg’s full geometry. - /// When used in Matrix request - will produce a durations matrix in response. - public static let expectedTravelTime = AttributeOptions(rawValue: 1 << 2) - - /// Current average speed (in meters per second) along the segment. - /// - /// When this attribute is specified, the ``RouteLeg/segmentSpeeds`` property contains one value for each segment in - /// the leg’s full geometry. This attribute is supported only by Directions and Map Matching requests. - public static let speed = AttributeOptions(rawValue: 1 << 3) - - /// Traffic congestion level along the segment. - /// - /// When this attribute is specified, the ``RouteLeg/segmentCongestionLevels`` property contains one value for each - /// segment - /// in the leg’s full geometry. - /// - /// This attribute requires ``ProfileIdentifier/automobileAvoidingTraffic`` and is supported only by Directions and - /// Map Matching requests. Any other profile identifier produces ``CongestionLevel/unknown`` for each segment along - /// the route. - public static let congestionLevel = AttributeOptions(rawValue: 1 << 4) - - /// The maximum speed limit along the segment. - /// - /// When this attribute is specified, the ``RouteLeg/segmentMaximumSpeedLimits`` property contains one value for - /// each segment in the leg’s full geometry. This attribute is supported only by Directions and Map Matching - /// requests. - public static let maximumSpeedLimit = AttributeOptions(rawValue: 1 << 5) - - /// Traffic congestion level in numeric form. - /// - /// When this attribute is specified, the ``RouteLeg/segmentNumericCongestionLevels`` property contains one value - /// for each - /// segment in the leg’s full geometry. - /// This attribute requires ``ProfileIdentifier/automobileAvoidingTraffic`` and is supported only by Directions and - /// Map Matching requests. Any other profile identifier produces `nil` for each segment along the route. - public static let numericCongestionLevel = AttributeOptions(rawValue: 1 << 6) - - /// The tendency value conveys the changing state of traffic congestion (increasing, decreasing, constant etc). - public static let trafficTendency = AttributeOptions(rawValue: 1 << 7) - - /// Creates an ``AttributeOptions`` from the given description strings. - public init?(descriptions: [String]) { - var attributeOptions: AttributeOptions = [] - for description in descriptions { - switch description { - case "closure": - attributeOptions.update(with: .closures) - case "distance": - attributeOptions.update(with: .distance) - case "duration": - attributeOptions.update(with: .expectedTravelTime) - case "speed": - attributeOptions.update(with: .speed) - case "congestion": - attributeOptions.update(with: .congestionLevel) - case "maxspeed": - attributeOptions.update(with: .maximumSpeedLimit) - case "congestion_numeric": - attributeOptions.update(with: .numericCongestionLevel) - case "traffic_tendency": - attributeOptions.update(with: .trafficTendency) - case "": - continue - default: - return nil - } - } - self.init(rawValue: attributeOptions.rawValue) - } - - public var description: String { - var descriptions: [String] = [] - if contains(.closures) { - descriptions.append("closure") - } - if contains(.distance) { - descriptions.append("distance") - } - if contains(.expectedTravelTime) { - descriptions.append("duration") - } - if contains(.speed) { - descriptions.append("speed") - } - if contains(.congestionLevel) { - descriptions.append("congestion") - } - if contains(.maximumSpeedLimit) { - descriptions.append("maxspeed") - } - if contains(.numericCongestionLevel) { - descriptions.append("congestion_numeric") - } - if contains(.trafficTendency) { - descriptions.append("traffic_tendency") - } - for (key, value) in customOptionsByRawValue { - if rawValue & key != 0 { - descriptions.append(value) - } - } - return descriptions.joined(separator: ",") - } -} - -extension AttributeOptions: Codable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(description.components(separatedBy: ",").filter { !$0.isEmpty }) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let descriptions = try container.decode([String].self) - self = AttributeOptions(descriptions: descriptions)! - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/BlockedLanes.swift b/ios/Classes/Navigation/MapboxDirections/BlockedLanes.swift deleted file mode 100644 index cb6f49b9e..000000000 --- a/ios/Classes/Navigation/MapboxDirections/BlockedLanes.swift +++ /dev/null @@ -1,134 +0,0 @@ - -import Foundation - -/// Defines a lane affected by the ``Incident`` -public struct BlockedLanes: OptionSet, CustomStringConvertible, Equatable, Sendable { - public var rawValue: Int - var stringKey: String? - - public init(rawValue: Int) { - self.init(rawValue: rawValue, key: nil) - } - - init(rawValue: Int, key: String?) { - self.rawValue = rawValue - self.stringKey = key - } - - /// Left lane - public static let left = BlockedLanes(rawValue: 1 << 0, key: "LEFT") - /// Left center lane - /// - /// Usually refers to the second lane from left on a four-lane highway - public static let leftCenter = BlockedLanes(rawValue: 1 << 1, key: "LEFT CENTER") - /// Left turn lane - public static let leftTurnLane = BlockedLanes(rawValue: 1 << 2, key: "LEFT TURN LANE") - /// Center lane - public static let center = BlockedLanes(rawValue: 1 << 3, key: "CENTER") - /// Right lane - public static let right = BlockedLanes(rawValue: 1 << 4, key: "RIGHT") - /// Right center lane - /// - /// Usually refers to the second lane from right on a four-lane highway - public static let rightCenter = BlockedLanes(rawValue: 1 << 5, key: "RIGHT CENTER") - /// Right turn lane - public static let rightTurnLane = BlockedLanes(rawValue: 1 << 6, key: "RIGHT TURN LANE") - /// High occupancy vehicle lane - public static let highOccupancyVehicle = BlockedLanes(rawValue: 1 << 7, key: "HOV") - /// Side lane - public static let side = BlockedLanes(rawValue: 1 << 8, key: "SIDE") - /// Shoulder lane - public static let shoulder = BlockedLanes(rawValue: 1 << 9, key: "SHOULDER") - /// Median lane - public static let median = BlockedLanes(rawValue: 1 << 10, key: "MEDIAN") - /// 1st Lane. - public static let lane1 = BlockedLanes(rawValue: 1 << 11, key: "1") - /// 2nd Lane. - public static let lane2 = BlockedLanes(rawValue: 1 << 12, key: "2") - /// 3rd Lane. - public static let lane3 = BlockedLanes(rawValue: 1 << 13, key: "3") - /// 4th Lane. - public static let lane4 = BlockedLanes(rawValue: 1 << 14, key: "4") - /// 5th Lane. - public static let lane5 = BlockedLanes(rawValue: 1 << 15, key: "5") - /// 6th Lane. - public static let lane6 = BlockedLanes(rawValue: 1 << 16, key: "6") - /// 7th Lane. - public static let lane7 = BlockedLanes(rawValue: 1 << 17, key: "7") - /// 8th Lane. - public static let lane8 = BlockedLanes(rawValue: 1 << 18, key: "8") - /// 9th Lane. - public static let lane9 = BlockedLanes(rawValue: 1 << 19, key: "9") - /// 10th Lane. - public static let lane10 = BlockedLanes(rawValue: 1 << 20, key: "10") - - static var allLanes: [BlockedLanes] { - return [ - .left, - .leftCenter, - .leftTurnLane, - .center, - .right, - .rightCenter, - .rightTurnLane, - .highOccupancyVehicle, - .side, - .shoulder, - .median, - .lane1, - .lane2, - .lane3, - .lane4, - .lane5, - .lane6, - .lane7, - .lane8, - .lane9, - .lane10, - ] - } - - /// Creates a ``BlockedLanes`` given an array of strings. - /// - /// Resulting options set will only contain known values. If string description does not match any known `Blocked - /// Lane` identifier - it will be ignored. - public init?(descriptions: [String]) { - var blockedLanes: BlockedLanes = [] - Self.allLanes.forEach { - if descriptions.contains($0.stringKey!) { - blockedLanes.insert($0) - } - } - self.init(rawValue: blockedLanes.rawValue) - } - - /// String representation of ``BlockedLanes`` options set. - /// - /// Resulting description contains only texts for known options. Custom options will be ignored if any. - public var description: String { - var descriptions: [String] = [] - Self.allLanes.forEach { - if contains($0) { - descriptions.append($0.stringKey!) - } - } - return descriptions.joined(separator: ",") - } -} - -extension BlockedLanes: Codable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(description.components(separatedBy: ",").filter { !$0.isEmpty }) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let descriptions = try container.decode([String].self) - if let roadClasses = BlockedLanes(descriptions: descriptions) { - self = roadClasses - } else { - throw DirectionsError.invalidResponse(nil) - } - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Congestion.swift b/ios/Classes/Navigation/MapboxDirections/Congestion.swift deleted file mode 100644 index 867b44d24..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Congestion.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -/// A ``CongestionLevel`` indicates the level of traffic congestion along a road segment relative to the normal flow of -/// traffic along that segment. You can color-code a route line according to the congestion level along each segment of -/// the route. -public enum CongestionLevel: String, Codable, CaseIterable, Equatable, Sendable { - /// There is not enough data to determine the level of congestion along the road segment. - case unknown - - /// The road segment has little or no congestion. Traffic is flowing smoothly. - /// - /// Low congestion levels are conventionally highlighted in green or not highlighted at all. - case low - - /// The road segment has moderate, stop-and-go congestion. Traffic is flowing but speed is impeded. - /// - /// Moderate congestion levels are conventionally highlighted in yellow. - case moderate - - /// The road segment has heavy, bumper-to-bumper congestion. Traffic is barely moving. - /// - /// Heavy congestion levels are conventionally highlighted in orange. - case heavy - - /// The road segment has severe congestion. Traffic may be completely stopped. - /// - /// Severe congestion levels are conventionally highlighted in red. - case severe -} - -/// `NumericCongestionLevel` is the level of traffic congestion along a road segment in numeric form, from 0-100. A -/// value of 0 indicates no congestion, a value of 100 indicates maximum congestion. -public typealias NumericCongestionLevel = Int diff --git a/ios/Classes/Navigation/MapboxDirections/Credentials.swift b/ios/Classes/Navigation/MapboxDirections/Credentials.swift deleted file mode 100644 index f220d01af..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Credentials.swift +++ /dev/null @@ -1,80 +0,0 @@ -import Foundation - -/// The Mapbox access token specified in the main application bundle’s Info.plist. -let defaultAccessToken: String? = - Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String ?? - Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAccessToken") as? String ?? - UserDefaults.standard.string(forKey: "MBXAccessToken") -let defaultApiEndPointURLString = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAPIBaseURL") as? String - -public struct Credentials: Equatable, Sendable { - /// The mapbox access token. You can find this in your Mapbox account dashboard. - public let accessToken: String? - - /// The host to reach. defaults to `api.mapbox.com`. - public let host: URL - - /// The SKU Token associated with the request. Used for billing. - public var skuToken: String? { -#if !os(Linux) - guard let mbx: AnyClass = NSClassFromString("MBXAccounts"), - mbx.responds(to: Selector(("serviceSkuToken"))), - let serviceSkuToken = mbx.value(forKeyPath: "serviceSkuToken") as? String - else { return nil } - - if mbx.responds(to: Selector(("serviceAccessToken"))) { - guard let serviceAccessToken = mbx.value(forKeyPath: "serviceAccessToken") as? String, - serviceAccessToken == accessToken - else { return nil } - - return serviceSkuToken - } else { - return serviceSkuToken - } -#else - return nil -#endif - } - - /// Intialize a new credential. - /// - Parameters: - /// - token: An access token to provide. If this value is nil, the SDK will attempt to find a token from your - /// app's `info.plist`. - /// - host: An optional parameter to pass a custom host. If `nil` is provided, the SDK will attempt to find a host - /// from your app's `info.plist`, and barring that will default to `https://api.mapbox.com`. - public init(accessToken token: String? = nil, host: URL? = nil) { - let accessToken = token ?? defaultAccessToken - - precondition( - accessToken != nil && !accessToken!.isEmpty, - "A Mapbox access token is required. Go to . In Info.plist, set the MBXAccessToken key to your access token, or use the Directions(accessToken:host:) initializer." - ) - self.accessToken = accessToken - if let host { - self.host = host - } else if let defaultHostString = defaultApiEndPointURLString, - let defaultHost = URL(string: defaultHostString) - { - self.host = defaultHost - } else { - self.host = URL(string: "https://api.mapbox.com")! - } - } - - /// Attempts to get ``host`` and ``accessToken`` from provided URL to create ``Credentials`` instance. - /// - /// If it is impossible to extract parameter(s) - default values will be used. - public init(requestURL url: URL) { - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) - let accessToken = components? - .queryItems? - .first { $0.name == "access_token" }? - .value - components?.path = "/" - components?.queryItems = nil - self.init(accessToken: accessToken, host: components?.url) - } -} - -@available(*, deprecated, renamed: "Credentials") -public typealias DirectionsCredentials = Credentials diff --git a/ios/Classes/Navigation/MapboxDirections/CustomValueOptionSet.swift b/ios/Classes/Navigation/MapboxDirections/CustomValueOptionSet.swift deleted file mode 100644 index 75b4e5dd8..000000000 --- a/ios/Classes/Navigation/MapboxDirections/CustomValueOptionSet.swift +++ /dev/null @@ -1,675 +0,0 @@ -import Foundation - -/// Describes how ``CustomValueOptionSet/customOptionsByRawValue`` component is compared during logical operations in -/// ``CustomValueOptionSet``. -public enum CustomOptionComparisonPolicy: Equatable, Sendable { - /// Custom options are equal if ``CustomValueOptionSet/customOptionsByRawValue`` key-value pairs are strictly equal - /// - /// Example: - /// [1: "value1"] == [1: "value1"] - /// [1: "value1"] != [1: "value2"] - /// [1: "value1"] != [:] - /// [:] == [:] - case equal - /// Custom options are equal if ``CustomValueOptionSet/customOptionsByRawValue`` by the given key is equal or `nil` - /// - /// Example: - /// [1: "value1"] == [1: "value1"] - /// [1: "value1"] != [1: "value2"] - /// [1: "value1"] == [:] - /// [:] == [:] - case equalOrNull - /// Custom options are not compared. Only `rawValue` is taken into account when comparing ``CustomValueOptionSet``s. - /// - /// Example: - /// [1: "value1"] == [1: "value1"] - /// [1: "value1"] == [1: "value2"] - /// [1: "value1"] == [:] - /// [:] == [:] - case rawValueEqual -} - -/// Option set implementation which allows each option to have custom string value attached. -public protocol CustomValueOptionSet: OptionSet where RawValue: FixedWidthInteger, Element == Self { - associatedtype Element = Self - associatedtype CustomValue: Equatable - var rawValue: Self.RawValue { get set } - - /// Provides a text value description for user-provided options. - /// - /// The option set will recognize a custom option if it's unique `rawValue` flag is set and - /// ``customOptionsByRawValue`` contains a description for that flag. - /// Use the ``update(customOption:comparisonPolicy:)`` method to append a custom option. - var customOptionsByRawValue: [RawValue: CustomValue] { get set } - - init(rawValue: Self.RawValue) - - /// Returns a Boolean value that indicates whether the given element exists - /// in the set. - /// - /// This example uses the `contains(_:)` method to test whether an integer is - /// a member of a set of prime numbers. - /// - /// let primes: Set = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37] - /// let x = 5 - /// if primes.contains(x) { - /// print("\(x) is prime!") - /// } else { - /// print("\(x). Not prime.") - /// } - /// // Prints "5 is prime!" - /// - /// - Parameter member: An element to look for in the set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: `true` if `member` exists in the set; otherwise, `false`. - func contains(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool - /// Returns a new set with the elements of both this and the given set. - /// - /// In the following example, the `attendeesAndVisitors` set is made up - /// of the elements of the `attendees` and `visitors` sets: - /// - /// let attendees: Set = ["Alicia", "Bethany", "Diana"] - /// let visitors = ["Marcia", "Nathaniel"] - /// let attendeesAndVisitors = attendees.union(visitors) - /// print(attendeesAndVisitors) - /// // Prints "["Diana", "Nathaniel", "Bethany", "Alicia", "Marcia"]" - /// - /// If the set already contains one or more elements that are also in - /// `other`, the existing members are kept. - /// - /// let initialIndices = Set(0..<5) - /// let expandedIndices = initialIndices.union([2, 3, 6, 7]) - /// print(expandedIndices) - /// // Prints "[2, 4, 6, 7, 0, 1, 3]" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: A new set with the unique elements of this set and `other`. - /// - /// - Note: if this set and `other` contain elements that are equal but - /// distinguishable (e.g. via `===`), which of these elements is present - /// in the result is unspecified. - func union(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element - /// Adds the elements of the given set to the set. - /// - /// In the following example, the elements of the `visitors` set are added to - /// the `attendees` set: - /// - /// var attendees: Set = ["Alicia", "Bethany", "Diana"] - /// let visitors: Set = ["Diana", "Marcia", "Nathaniel"] - /// attendees.formUnion(visitors) - /// print(attendees) - /// // Prints "["Diana", "Nathaniel", "Bethany", "Alicia", "Marcia"]" - /// - /// If the set already contains one or more elements that are also in - /// `other`, the existing members are kept. - /// - /// var initialIndices = Set(0..<5) - /// initialIndices.formUnion([2, 3, 6, 7]) - /// print(initialIndices) - /// // Prints "[2, 4, 6, 7, 0, 1, 3]" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - mutating func formUnion(_ other: Self, comparisonPolicy: CustomOptionComparisonPolicy) - /// Returns a new set with the elements that are common to both this set and - /// the given set. - /// - /// In the following example, the `bothNeighborsAndEmployees` set is made up - /// of the elements that are in *both* the `employees` and `neighbors` sets. - /// Elements that are in only one or the other are left out of the result of - /// the intersection. - /// - /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] - /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] - /// let bothNeighborsAndEmployees = employees.intersection(neighbors) - /// print(bothNeighborsAndEmployees) - /// // Prints "["Bethany", "Eric"]" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: A new set. - /// - /// - Note: if this set and `other` contain elements that are equal but - /// distinguishable (e.g. via `===`), which of these elements is present - /// in the result is unspecified. - func intersection(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element - /// Removes the elements of this set that aren't also in the given set. - /// - /// In the following example, the elements of the `employees` set that are - /// not also members of the `neighbors` set are removed. In particular, the - /// names `"Alicia"`, `"Chris"`, and `"Diana"` are removed. - /// - /// var employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] - /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] - /// employees.formIntersection(neighbors) - /// print(employees) - /// // Prints "["Bethany", "Eric"]" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - mutating func formIntersection(_ other: Self, comparisonPolicy: CustomOptionComparisonPolicy) - /// Returns a new set with the elements that are either in this set or in the - /// given set, but not in both. - /// - /// In the following example, the `eitherNeighborsOrEmployees` set is made up - /// of the elements of the `employees` and `neighbors` sets that are not in - /// both `employees` *and* `neighbors`. In particular, the names `"Bethany"` - /// and `"Eric"` do not appear in `eitherNeighborsOrEmployees`. - /// - /// let employees: Set = ["Alicia", "Bethany", "Diana", "Eric"] - /// let neighbors: Set = ["Bethany", "Eric", "Forlani"] - /// let eitherNeighborsOrEmployees = employees.symmetricDifference(neighbors) - /// print(eitherNeighborsOrEmployees) - /// // Prints "["Diana", "Forlani", "Alicia"]" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: A new set. - func symmetricDifference(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element - /// Removes the elements of the set that are also in the given set and adds - /// the members of the given set that are not already in the set. - /// - /// In the following example, the elements of the `employees` set that are - /// also members of `neighbors` are removed from `employees`, while the - /// elements of `neighbors` that are not members of `employees` are added to - /// `employees`. In particular, the names `"Bethany"` and `"Eric"` are - /// removed from `employees` while the name `"Forlani"` is added. - /// - /// var employees: Set = ["Alicia", "Bethany", "Diana", "Eric"] - /// let neighbors: Set = ["Bethany", "Eric", "Forlani"] - /// employees.formSymmetricDifference(neighbors) - /// print(employees) - /// // Prints "["Diana", "Forlani", "Alicia"]" - /// - /// - Parameter other: A set of the same type. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - mutating func formSymmetricDifference(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) - /// Returns a new set containing the elements of this set that do not occur - /// in the given set. - /// - /// In the following example, the `nonNeighbors` set is made up of the - /// elements of the `employees` set that are not elements of `neighbors`: - /// - /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] - /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] - /// let nonNeighbors = employees.subtracting(neighbors) - /// print(nonNeighbors) - /// // Prints "["Diana", "Chris", "Alicia"]" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: A new set. - func subtracting(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element - /// Removes the elements of the given set from this set. - /// - /// In the following example, the elements of the `employees` set that are - /// also members of the `neighbors` set are removed. In particular, the - /// names `"Bethany"` and `"Eric"` are removed from `employees`. - /// - /// var employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] - /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] - /// employees.subtract(neighbors) - /// print(employees) - /// // Prints "["Diana", "Chris", "Alicia"]" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - mutating func subtract(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) - /// Inserts the given element in the set if it is not already present. - /// - /// If an element equal to `newMember` is already contained in the set, this method has no effect. In this example, - /// a new element is inserted into `classDays`, a set of days of the week. When an existing element is inserted, the - /// `classDays` set does not change. - /// - /// enum DayOfTheWeek: Int { - /// case sunday, monday, tuesday, wednesday, thursday, - /// friday, saturday - /// } - /// - /// var classDays: Set = [.wednesday, .friday] - /// print(classDays.insert(.monday)) - /// // Prints "(true, .monday)" - /// print(classDays) - /// // Prints "[.friday, .wednesday, .monday]" - /// - /// print(classDays.insert(.friday)) - /// // Prints "(false, .friday)" - /// print(classDays) - /// // Prints "[.friday, .wednesday, .monday]" - /// - /// - Parameter newMember: An element to insert into the set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: `(true, newMember)` if `newMember` was not contained in the set. If an element equal to `newMember` - /// was already contained in the set, the method returns `(false, oldMember)`, where `oldMember` is the element that - /// was equal to `newMember`. In some cases, `oldMember` may be distinguishable from `newMember` by identity - /// comparison or some other means. - mutating func insert(_ newMember: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) - -> (inserted: Bool, memberAfterInsert: Self.Element) - /// Removes the given element and any elements subsumed by the given element. - /// - /// - Parameter member: The element of the set to remove. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: For ordinary sets, an element equal to `member` if `member` is contained in the set; otherwise, - /// `nil`. In some cases, a returned element may be distinguishable from `member` by identity comparison or some - /// other means. - /// - /// For sets where the set type and element type are the same, like - /// `OptionSet` types, this method returns any intersection between the set - /// and `[member]`, or `nil` if the intersection is empty. - mutating func remove(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element? - /// Inserts the given element into the set unconditionally. - /// - /// If an element equal to `newMember` is already contained in the set, - /// `newMember` replaces the existing element. In this example, an existing - /// element is inserted into `classDays`, a set of days of the week. - /// - /// enum DayOfTheWeek: Int { - /// case sunday, monday, tuesday, wednesday, thursday, - /// friday, saturday - /// } - /// - /// var classDays: Set = [.monday, .wednesday, .friday] - /// print(classDays.update(with: .monday)) - /// // Prints "Optional(.monday)" - /// - /// - Parameter newMember: An element to insert into the set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: For ordinary sets, an element equal to `newMember` if the set - /// already contained such a member; otherwise, `nil`. In some cases, the - /// returned element may be distinguishable from `newMember` by identity - /// comparison or some other means. - /// - /// For sets where the set type and element type are the same, like - /// `OptionSet` types, this method returns any intersection between the - /// set and `[newMember]`, or `nil` if the intersection is empty. - mutating func update(with newMember: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element? - /// Inserts the given element into the set unconditionally. - /// - /// If an element equal to `customOption` is already contained in the set, `customOption` replaces the existing - /// element. Otherwise - updates the set contents and fills ``customOptionsByRawValue`` accordingly. - /// - /// - Parameter customOption: An element to insert into the set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: For ordinary sets, an element equal to `customOption` if the set already contained such a member; - /// otherwise, `nil`. In some cases, the returned element may be distinguishable from `customOption` by identity - /// comparison or some other means. - /// - /// For sets where the set type and element type are the same, like - /// `OptionSet` types, this method returns any intersection between the - /// set and `[customOption]`, or `nil` if the intersection is empty. - mutating func update(customOption: (RawValue, CustomValue), comparisonPolicy: CustomOptionComparisonPolicy) -> Self - .Element? - /// Returns a Boolean value that indicates whether the set is a subset of - /// another set. - /// - /// Set *A* is a subset of another set *B* if every member of *A* is also a - /// member of *B*. - /// - /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] - /// let attendees: Set = ["Alicia", "Bethany", "Diana"] - /// print(attendees.isSubset(of: employees)) - /// // Prints "true" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. - func isSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool - /// Returns a Boolean value that indicates whether the set is a superset of - /// the given set. - /// - /// Set *A* is a superset of another set *B* if every member of *B* is also a - /// member of *A*. - /// - /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] - /// let attendees: Set = ["Alicia", "Bethany", "Diana"] - /// print(employees.isSuperset(of: attendees)) - /// // Prints "true" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: `true` if the set is a superset of `possibleSubset`; - /// otherwise, `false`. - func isSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool - /// Returns a Boolean value that indicates whether this set is a strict - /// subset of the given set. - /// - /// Set *A* is a strict subset of another set *B* if every member of *A* is - /// also a member of *B* and *B* contains at least one element that is not a - /// member of *A*. - /// - /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] - /// let attendees: Set = ["Alicia", "Bethany", "Diana"] - /// print(attendees.isStrictSubset(of: employees)) - /// // Prints "true" - /// - /// // A set is never a strict subset of itself: - /// print(attendees.isStrictSubset(of: attendees)) - /// // Prints "false" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: `true` if the set is a strict subset of `other`; otherwise, - /// `false`. - func isStrictSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool - /// Returns a Boolean value that indicates whether this set is a strict - /// superset of the given set. - /// - /// Set *A* is a strict superset of another set *B* if every member of *B* is - /// also a member of *A* and *A* contains at least one element that is *not* - /// a member of *B*. - /// - /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] - /// let attendees: Set = ["Alicia", "Bethany", "Diana"] - /// print(employees.isStrictSuperset(of: attendees)) - /// // Prints "true" - /// - /// // A set is never a strict superset of itself: - /// print(employees.isStrictSuperset(of: employees)) - /// // Prints "false" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: `true` if the set is a strict superset of `other`; otherwise, - /// `false`. - func isStrictSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool - /// Returns a Boolean value that indicates whether the set has no members in - /// common with the given set. - /// - /// In the following example, the `employees` set is disjoint with the - /// `visitors` set because no name appears in both sets. - /// - /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] - /// let visitors: Set = ["Marcia", "Nathaniel", "Olivia"] - /// print(employees.isDisjoint(with: visitors)) - /// // Prints "true" - /// - /// - Parameter other: A set of the same type as the current set. - /// - Parameter comparisonPolicy: comparison method to be used for ``customOptionsByRawValue``. - /// - Returns: `true` if the set has no elements in common with `other`; - /// otherwise, `false`. - func isDisjoint(with other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool -} - -extension CustomValueOptionSet where Self == Self.Element { - // MARK: Implemented methods - - private func customOptionIsEqual( - _ lhs: [RawValue: CustomValue], - _ rhs: [RawValue: CustomValue], - key: RawValue, - policy: CustomOptionComparisonPolicy - ) -> Bool { - switch policy { - case .equal: - return lhs[key] == rhs[key] - case .equalOrNull: - return lhs[key] == rhs[key] || lhs[key] == nil || rhs[key] == nil - case .rawValueEqual: - return true - } - } - - @discardableResult - public func contains(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { - let intersection = rawValue & member.rawValue - guard intersection != 0 else { - return false - } - - for offset in 0.. (inserted: Bool, memberAfterInsert: Self.Element) { - if contains(newMember, comparisonPolicy: comparisonPolicy) { - return (false, intersection(newMember, comparisonPolicy: comparisonPolicy)) - } else { - rawValue = rawValue | newMember.rawValue - customOptionsByRawValue.merge(newMember.customOptionsByRawValue) { current, _ in current } - return (true, newMember) - } - } - - @discardableResult @inlinable - public mutating func remove(_ member: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self - .Element? { - let intersection = intersection(member, comparisonPolicy: comparisonPolicy) - if intersection.rawValue == 0 { - return nil - } else { - rawValue -= intersection.rawValue - customOptionsByRawValue = customOptionsByRawValue.filter { key, _ in - rawValue & key != 0 - } - return intersection - } - } - - @discardableResult @inlinable - public mutating func update(with newMember: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self - .Element? { - let intersection = intersection(newMember, comparisonPolicy: comparisonPolicy) - - if intersection.rawValue == 0 { - // insert - rawValue = rawValue | newMember.rawValue - customOptionsByRawValue.merge(newMember.customOptionsByRawValue) { current, _ in current } - return nil - } else { - // update - rawValue = rawValue | newMember.rawValue - customOptionsByRawValue.merge(intersection.customOptionsByRawValue) { _, new in new } - return intersection - } - } - - public mutating func formIntersection(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) { - rawValue = rawValue & other.rawValue - customOptionsByRawValue = customOptionsByRawValue.reduce(into: [:]) { partialResult, item in - if customOptionIsEqual( - customOptionsByRawValue, - other.customOptionsByRawValue, - key: item.key, - policy: comparisonPolicy - ) { - partialResult[item.key] = item.value - } else if rawValue & item.key != 0 { - rawValue -= item.key - } - } - } - - public mutating func subtract(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) { - rawValue = rawValue ^ (rawValue & other.rawValue) - customOptionsByRawValue = customOptionsByRawValue.reduce(into: [:]) { partialResult, item in - if !customOptionIsEqual( - customOptionsByRawValue, - other.customOptionsByRawValue, - key: item.key, - policy: comparisonPolicy - ) { - partialResult[item.key] = item.value - } - } - } - - // MARK: Deferring methods - - @discardableResult @inlinable - public func union(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element { - var union = self - union.formUnion(other, comparisonPolicy: comparisonPolicy) - return union - } - - @discardableResult @inlinable - public func intersection(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element { - var intersection = self - intersection.formIntersection(other, comparisonPolicy: comparisonPolicy) - return intersection - } - - @discardableResult @inlinable - public func symmetricDifference(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self - .Element { - var difference = self - difference.formSymmetricDifference(other, comparisonPolicy: comparisonPolicy) - return difference - } - - @discardableResult @inlinable - public mutating func update( - customOption: (RawValue, CustomValue), - comparisonPolicy: CustomOptionComparisonPolicy - ) -> Self.Element? { - var newMember = Self(rawValue: customOption.0) - newMember.customOptionsByRawValue[customOption.0] = customOption.1 - return update(with: newMember, comparisonPolicy: comparisonPolicy) - } - - @inlinable - public mutating func formUnion(_ other: Self, comparisonPolicy: CustomOptionComparisonPolicy) { - _ = update(with: other, comparisonPolicy: comparisonPolicy) - } - - @inlinable - public mutating func formSymmetricDifference( - _ other: Self.Element, - comparisonPolicy: CustomOptionComparisonPolicy - ) { - let intersection = intersection(other, comparisonPolicy: comparisonPolicy) - _ = remove(other, comparisonPolicy: comparisonPolicy) - _ = insert( - other.subtracting( - intersection, - comparisonPolicy: comparisonPolicy - ), - comparisonPolicy: comparisonPolicy - ) - } - - @discardableResult @inlinable - public func subtracting(_ other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Self.Element { - var substracted = self - substracted.subtract(other, comparisonPolicy: comparisonPolicy) - return substracted - } - - @discardableResult @inlinable - public func isSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { - return intersection(other, comparisonPolicy: comparisonPolicy) == self - } - - @discardableResult @inlinable - public func isDisjoint(with other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { - return intersection(other, comparisonPolicy: comparisonPolicy).isEmpty - } - - @discardableResult @inlinable - public func isSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { - return other.isSubset(of: self, comparisonPolicy: comparisonPolicy) - } - - @discardableResult @inlinable - public func isStrictSuperset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { - return isSuperset(of: other, comparisonPolicy: comparisonPolicy) && rawValue > other.rawValue - } - - @discardableResult @inlinable - public func isStrictSubset(of other: Self.Element, comparisonPolicy: CustomOptionComparisonPolicy) -> Bool { - return other.isStrictSuperset(of: self, comparisonPolicy: comparisonPolicy) - } -} - -// MARK: - SetAlgebra implementation - -extension CustomValueOptionSet { - @discardableResult @inlinable - public func contains(_ member: Self.Element) -> Bool { - return contains(member, comparisonPolicy: .equal) - } - - @discardableResult @inlinable - public func union(_ other: Self) -> Self { - return union(other, comparisonPolicy: .equal) - } - - @discardableResult @inlinable - public func intersection(_ other: Self) -> Self { - return intersection(other, comparisonPolicy: .equal) - } - - @discardableResult @inlinable - public func symmetricDifference(_ other: Self) -> Self { - return symmetricDifference(other, comparisonPolicy: .equal) - } - - @discardableResult @inlinable - public mutating func insert(_ newMember: Self.Element) -> (inserted: Bool, memberAfterInsert: Self.Element) { - return insert(newMember, comparisonPolicy: .equal) - } - - @discardableResult @inlinable - public mutating func remove(_ member: Self.Element) -> Self.Element? { - return remove(member, comparisonPolicy: .equal) - } - - @discardableResult @inlinable - public mutating func update(with newMember: Self.Element) -> Self.Element? { - return update(with: newMember, comparisonPolicy: .equal) - } - - @inlinable - public mutating func formUnion(_ other: Self) { - formUnion(other, comparisonPolicy: .equal) - } - - @inlinable - public mutating func formIntersection(_ other: Self) { - formIntersection(other, comparisonPolicy: .equal) - } - - @inlinable - public mutating func formSymmetricDifference(_ other: Self) { - formSymmetricDifference(other, comparisonPolicy: .equal) - } - - @discardableResult @inlinable - public func subtracting(_ other: Self) -> Self { - return subtracting(other, comparisonPolicy: .equal) - } - - @discardableResult @inlinable - public func isSubset(of other: Self) -> Bool { - return isSubset(of: other, comparisonPolicy: .equal) - } - - @discardableResult @inlinable - public func isDisjoint(with other: Self) -> Bool { - return isDisjoint(with: other, comparisonPolicy: .equal) - } - - @discardableResult @inlinable - public func isSuperset(of other: Self) -> Bool { - return isSuperset(of: other, comparisonPolicy: .equal) - } - - @inlinable - public mutating func subtract(_ other: Self) { - subtract(other, comparisonPolicy: .equal) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Directions.swift b/ios/Classes/Navigation/MapboxDirections/Directions.swift deleted file mode 100644 index 18b89fd45..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Directions.swift +++ /dev/null @@ -1,711 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -typealias JSONDictionary = [String: Any] - -/// Indicates that an error occurred in MapboxDirections. -public let MBDirectionsErrorDomain = "com.mapbox.directions.ErrorDomain" - -/// A `Directions` object provides you with optimal directions between different locations, or waypoints. The directions -/// object passes your request to the [Mapbox Directions API](https://docs.mapbox.com/api/navigation/#directions) and -/// returns the requested information to a closure (block) that you provide. A directions object can handle multiple -/// simultaneous requests. A ``RouteOptions`` object specifies criteria for the results, such as intermediate waypoints, -/// a mode of transportation, or the level of detail to be returned. -/// -/// Each result produced by the directions object is stored in a ``Route`` object. Depending on the ``RouteOptions`` -/// object you provide, each route may include detailed information suitable for turn-by-turn directions, or it may -/// include only high-level information such as the distance, estimated travel time, and name of each leg of the trip. -/// The waypoints that form the request may be conflated with nearby locations, as appropriate; the resulting waypoints -/// are provided to the closure. -@_documentation(visibility: internal) -open class Directions: @unchecked Sendable { - /// A closure (block) to be called when a directions request is complete. - /// - /// - Parameter result: A `Result` enum that represents the ``RouteResponse`` if the request returned successfully, - /// or the error if it did not. - public typealias RouteCompletionHandler = @Sendable ( - _ result: Result - ) -> Void - - /// A closure (block) to be called when a map matching request is complete. - /// - /// - Parameter result: A `Result` enum that represents the ``MapMatchingResponse`` if the request returned - /// successfully, or the error if it did not. - public typealias MatchCompletionHandler = @Sendable ( - _ result: Result - ) -> Void - - /// A closure (block) to be called when a directions refresh request is complete. - /// - /// - parameter credentials: An object containing the credentials used to make the request. - /// - parameter result: A `Result` enum that represents the ``RouteRefreshResponse`` if the request returned - /// successfully, or the error if it did not. - public typealias RouteRefreshCompletionHandler = @Sendable ( - _ credentials: Credentials, - _ result: Result - ) -> Void - - // MARK: Creating a Directions Object - - /// The shared directions object. - /// - /// To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be - /// specified in the `MBXAccessToken` key in the main application bundle’s Info.plist. - public static let shared: Directions = .init() - - /// The Authorization & Authentication credentials that are used for this service. - /// - /// If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. - public let credentials: Credentials - - private let urlSession: URLSession - private let processingQueue: DispatchQueue - - /// Creates a new instance of Directions object. - /// - Parameters: - /// - credentials: Credentials that will be used to make API requests to Mapbox Directions API. - /// - urlSession: URLSession that will be used to submit API requests to Mapbox Directions API. - /// - processingQueue: A DispatchQueue that will be used for CPU intensive work. - public init( - credentials: Credentials = .init(), - urlSession: URLSession = .shared, - processingQueue: DispatchQueue = .global(qos: .userInitiated) - ) { - self.credentials = credentials - self.urlSession = urlSession - self.processingQueue = processingQueue - } - - // MARK: Getting Directions - - /// Begins asynchronously calculating routes using the given options and delivers the results to a closure. - /// - /// This method retrieves the routes asynchronously from the [Mapbox Directions - /// API](https://www.mapbox.com/api-documentation/navigation/#directions) over a network connection. If a connection - /// error or server error occurs, details about the error are passed into the given completion handler in lieu of - /// the routes. - /// - /// Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). - /// - Parameters: - /// - options: A ``RouteOptions`` object specifying the requirements for the resulting routes. - /// - completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the - /// application’s main thread. - /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to - /// execute, you no longer want the resulting routes, cancel this task. - @discardableResult - open func calculate( - _ options: RouteOptions, - completionHandler: @escaping RouteCompletionHandler - ) -> URLSessionDataTask { - options.fetchStartDate = Date() - let request = urlRequest(forCalculating: options) - let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in - - if let urlError = possibleError as? URLError { - completionHandler(.failure(.network(urlError))) - return - } - - guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - completionHandler(.failure(.invalidResponse(possibleResponse))) - return - } - - guard let data = possibleData else { - completionHandler(.failure(.noData)) - return - } - - self.processingQueue.async { - do { - let decoder = JSONDecoder() - decoder.userInfo = [ - .options: options, - .credentials: self.credentials, - ] - - guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { - let apiError = DirectionsError( - code: nil, - message: nil, - response: possibleResponse, - underlyingError: possibleError - ) - completionHandler(.failure(apiError)) - return - } - - guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { - let apiError = DirectionsError( - code: disposition.code, - message: disposition.message, - response: response, - underlyingError: possibleError - ) - completionHandler(.failure(apiError)) - return - } - - let result = try decoder.decode(RouteResponse.self, from: data) - guard result.routes != nil else { - completionHandler(.failure(.unableToRoute)) - return - } - - completionHandler(.success(result)) - } catch { - let bailError = DirectionsError(code: nil, message: nil, response: response, underlyingError: error) - completionHandler(.failure(bailError)) - } - } - } - requestTask.priority = 1 - requestTask.resume() - - return requestTask - } - - /// Begins asynchronously calculating matches using the given options and delivers the results to a closure.This - /// method retrieves the matches asynchronously from the [Mapbox Map Matching - /// API](https://docs.mapbox.com/api/navigation/#map-matching) over a network connection. If a connection error or - /// server error occurs, details about the error are passed into the given completion handler in lieu of the routes. - /// - /// To get ``Route``s based on these matches, use the `calculateRoutes(matching:completionHandler:)` method - /// instead. - /// - Parameters: - /// - options: A ``MatchOptions`` object specifying the requirements for the resulting matches. - /// - completionHandler: The closure (block) to call with the resulting matches. This closure is executed on the - /// application’s main thread. - /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to - /// execute, you no longer want the resulting matches, cancel this task. - @discardableResult - open func calculate( - _ options: MatchOptions, - completionHandler: @escaping MatchCompletionHandler - ) -> URLSessionDataTask { - options.fetchStartDate = Date() - let request = urlRequest(forCalculating: options) - let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in - if let urlError = possibleError as? URLError { - completionHandler(.failure(.network(urlError))) - return - } - - guard let response = possibleResponse, response.mimeType == "application/json" else { - completionHandler(.failure(.invalidResponse(possibleResponse))) - return - } - - guard let data = possibleData else { - completionHandler(.failure(.noData)) - return - } - - self.processingQueue.async { - do { - let decoder = JSONDecoder() - decoder.userInfo = [ - .options: options, - .credentials: self.credentials, - ] - guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { - let apiError = DirectionsError( - code: nil, - message: nil, - response: possibleResponse, - underlyingError: possibleError - ) - completionHandler(.failure(apiError)) - return - } - - guard disposition.code == "Ok" else { - let apiError = DirectionsError( - code: disposition.code, - message: disposition.message, - response: response, - underlyingError: possibleError - ) - completionHandler(.failure(apiError)) - return - } - - let response = try decoder.decode(MapMatchingResponse.self, from: data) - - guard response.matches != nil else { - completionHandler(.failure(.unableToRoute)) - return - } - - completionHandler(.success(response)) - } catch { - let caughtError = DirectionsError.unknown( - response: response, - underlying: error, - code: nil, - message: nil - ) - completionHandler(.failure(caughtError)) - } - } - } - requestTask.priority = 1 - requestTask.resume() - - return requestTask - } - - /// Begins asynchronously calculating routes that match the given options and delivers the results to a closure. - /// - /// This method retrieves the routes asynchronously from the [Mapbox Map Matching - /// API](https://docs.mapbox.com/api/navigation/#map-matching) over a network connection. If a connection error or - /// server error occurs, details about the error are passed into the given completion handler in lieu of the routes. - /// - /// To get the ``Match``es that these routes are based on, use the `calculate(_:completionHandler:)` method instead. - /// - Parameters: - /// - options: A ``MatchOptions`` object specifying the requirements for the resulting match. - /// - completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the - /// application’s main thread. - /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to - /// execute, you no longer want the resulting routes, cancel this task. - @discardableResult - open func calculateRoutes( - matching options: MatchOptions, - completionHandler: @escaping RouteCompletionHandler - ) -> URLSessionDataTask { - options.fetchStartDate = Date() - let request = urlRequest(forCalculating: options) - let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in - if let urlError = possibleError as? URLError { - completionHandler(.failure(.network(urlError))) - return - } - - guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - completionHandler(.failure(.invalidResponse(possibleResponse))) - return - } - - guard let data = possibleData else { - completionHandler(.failure(.noData)) - return - } - - self.processingQueue.async { - do { - let decoder = JSONDecoder() - decoder.userInfo = [ - .options: options, - .credentials: self.credentials, - ] - - guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { - let apiError = DirectionsError( - code: nil, - message: nil, - response: possibleResponse, - underlyingError: possibleError - ) - completionHandler(.failure(apiError)) - return - } - - guard disposition.code == "Ok" else { - let apiError = DirectionsError( - code: disposition.code, - message: disposition.message, - response: response, - underlyingError: possibleError - ) - completionHandler(.failure(apiError)) - return - } - - let result = try decoder.decode(MapMatchingResponse.self, from: data) - - let routeResponse = try RouteResponse( - matching: result, - options: options, - credentials: self.credentials - ) - guard routeResponse.routes != nil else { - completionHandler(.failure(.unableToRoute)) - return - } - - completionHandler(.success(routeResponse)) - } catch { - let bailError = DirectionsError(code: nil, message: nil, response: response, underlyingError: error) - completionHandler(.failure(bailError)) - } - } - } - requestTask.priority = 1 - requestTask.resume() - - return requestTask - } - - /// Begins asynchronously refreshing the route with the given identifier, optionally starting from an arbitrary leg - /// along the route. - /// - /// This method retrieves skeleton route data asynchronously from the Mapbox Directions Refresh API over a network - /// connection. If a connection error or server error occurs, details about the error are passed into the given - /// completion handler in lieu of the routes. - /// - /// - Precondition: Set ``RouteOptions/refreshingEnabled`` to `true` when calculating the original route. - /// - Parameters: - /// - responseIdentifier: The ``RouteResponse/identifier`` value of the ``RouteResponse`` that contains the route - /// to refresh. - /// - routeIndex: The index of the route to refresh in the original ``RouteResponse/routes`` array. - /// - startLegIndex: The index of the leg in the route at which to begin refreshing. The response will omit any - /// leg before this index and refresh any leg from this index to the end of the route. If this argument is omitted, - /// the entire route is refreshed. - /// - completionHandler: The closure (block) to call with the resulting skeleton route data. This closure is - /// executed on the application’s main thread. - /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to - /// execute, you no longer want the resulting skeleton routes, cancel this task. - @discardableResult - open func refreshRoute( - responseIdentifier: String, - routeIndex: Int, - fromLegAtIndex startLegIndex: Int = 0, - completionHandler: @escaping RouteRefreshCompletionHandler - ) -> URLSessionDataTask? { - _refreshRoute( - responseIdentifier: responseIdentifier, - routeIndex: routeIndex, - fromLegAtIndex: startLegIndex, - currentRouteShapeIndex: nil, - completionHandler: completionHandler - ) - } - - /// Begins asynchronously refreshing the route with the given identifier, optionally starting from an arbitrary leg - /// and point along the route. - /// - /// This method retrieves skeleton route data asynchronously from the Mapbox Directions Refresh API over a network - /// connection. If a connection error or server error occurs, details about the error are passed into the given - /// completion handler in lieu of the routes. - /// - /// - Precondition: Set ``RouteOptions/refreshingEnabled`` to `true` when calculating the original route. - /// - Parameters: - /// - responseIdentifier: The ``RouteResponse/identifier`` value of the ``RouteResponse`` that contains the route - /// to refresh. - /// - routeIndex: The index of the route to refresh in the original ``RouteResponse/routes`` array. - /// - startLegIndex: The index of the leg in the route at which to begin refreshing. The response will omit any - /// leg before this index and refresh any leg from this index to the end of the route. If this argument is omitted, - /// the entire route is refreshed. - /// - currentRouteShapeIndex: The index of the route geometry at which to begin refreshing. Indexed geometry must - /// be contained by the leg at `startLegIndex`. - /// - completionHandler: The closure (block) to call with the resulting skeleton route data. This closure is - /// executed on the application’s main thread. - /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to - /// execute, you no longer want the resulting skeleton routes, cancel this task. - @discardableResult - open func refreshRoute( - responseIdentifier: String, - routeIndex: Int, - fromLegAtIndex startLegIndex: Int = 0, - currentRouteShapeIndex: Int, - completionHandler: @escaping RouteRefreshCompletionHandler - ) -> URLSessionDataTask? { - _refreshRoute( - responseIdentifier: responseIdentifier, - routeIndex: routeIndex, - fromLegAtIndex: startLegIndex, - currentRouteShapeIndex: currentRouteShapeIndex, - completionHandler: completionHandler - ) - } - - private func _refreshRoute( - responseIdentifier: String, - routeIndex: Int, - fromLegAtIndex startLegIndex: Int, - currentRouteShapeIndex: Int?, - completionHandler: @escaping RouteRefreshCompletionHandler - ) -> URLSessionDataTask? { - let request: URLRequest = if let currentRouteShapeIndex { - urlRequest( - forRefreshing: responseIdentifier, - routeIndex: routeIndex, - fromLegAtIndex: startLegIndex, - currentRouteShapeIndex: currentRouteShapeIndex - ) - } else { - urlRequest( - forRefreshing: responseIdentifier, - routeIndex: routeIndex, - fromLegAtIndex: startLegIndex - ) - } - let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in - if let urlError = possibleError as? URLError { - DispatchQueue.main.async { - completionHandler(self.credentials, .failure(.network(urlError))) - } - return - } - - guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - DispatchQueue.main.async { - completionHandler(self.credentials, .failure(.invalidResponse(possibleResponse))) - } - return - } - - guard let data = possibleData else { - DispatchQueue.main.async { - completionHandler(self.credentials, .failure(.noData)) - } - return - } - - self.processingQueue.async { - do { - let decoder = JSONDecoder() - decoder.userInfo = [ - .responseIdentifier: responseIdentifier, - .routeIndex: routeIndex, - .startLegIndex: startLegIndex, - .credentials: self.credentials, - ] - - guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { - let apiError = DirectionsError( - code: nil, - message: nil, - response: possibleResponse, - underlyingError: possibleError - ) - - DispatchQueue.main.async { - completionHandler(self.credentials, .failure(apiError)) - } - return - } - - guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { - let apiError = DirectionsError( - code: disposition.code, - message: disposition.message, - response: response, - underlyingError: possibleError - ) - DispatchQueue.main.async { - completionHandler(self.credentials, .failure(apiError)) - } - return - } - - let result = try decoder.decode(RouteRefreshResponse.self, from: data) - - DispatchQueue.main.async { - completionHandler(self.credentials, .success(result)) - } - } catch { - DispatchQueue.main.async { - let bailError = DirectionsError( - code: nil, - message: nil, - response: response, - underlyingError: error - ) - completionHandler(self.credentials, .failure(bailError)) - } - } - } - } - requestTask.priority = 1 - requestTask.resume() - return requestTask - } - - open func urlRequest( - forRefreshing responseIdentifier: String, - routeIndex: Int, - fromLegAtIndex startLegIndex: Int - ) -> URLRequest { - _urlRequest( - forRefreshing: responseIdentifier, - routeIndex: routeIndex, - fromLegAtIndex: startLegIndex, - currentRouteShapeIndex: nil - ) - } - - open func urlRequest( - forRefreshing responseIdentifier: String, - routeIndex: Int, - fromLegAtIndex startLegIndex: Int, - currentRouteShapeIndex: Int - ) -> URLRequest { - _urlRequest( - forRefreshing: responseIdentifier, - routeIndex: routeIndex, - fromLegAtIndex: startLegIndex, - currentRouteShapeIndex: currentRouteShapeIndex - ) - } - - private func _urlRequest( - forRefreshing responseIdentifier: String, - routeIndex: Int, - fromLegAtIndex startLegIndex: Int, - currentRouteShapeIndex: Int? - ) -> URLRequest { - var params: [URLQueryItem] = credentials.authenticationParams - if let currentRouteShapeIndex { - params.append(URLQueryItem(name: "current_route_geometry_index", value: String(currentRouteShapeIndex))) - } - - var unparameterizedURL = URL( - string: "directions-refresh/v1/\(ProfileIdentifier.automobileAvoidingTraffic.rawValue)", - relativeTo: credentials.host - )! - unparameterizedURL.appendPathComponent(responseIdentifier) - unparameterizedURL.appendPathComponent(String(routeIndex)) - unparameterizedURL.appendPathComponent(String(startLegIndex)) - var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! - - components.queryItems = params - - let getURL = components.url! - var request = URLRequest(url: getURL) - request.setupUserAgentString() - return request - } - - /// The GET HTTP URL used to fetch the routes from the API. - /// - /// After requesting the URL returned by this method, you can parse the JSON data in the response and pass it into - /// the ``Route/init(from:)`` initializer. Alternatively, you can use the ``calculate(_:completionHandler:)-8je4q`` - /// method, which automatically sends the request and parses the response. - /// - Parameter options: A ``DirectionsOptions`` object specifying the requirements for the resulting routes. - /// - Returns: The URL to send the request to. - open func url(forCalculating options: DirectionsOptions) -> URL { - return url(forCalculating: options, httpMethod: "GET") - } - - /// The HTTP URL used to fetch the routes from the API using the specified HTTP method. - /// - /// The query part of the URL is generally suitable for GET requests. However, if the URL is exceptionally long, it - /// may be more appropriate to send a POST request to a URL without the query part, relegating the query to the body - /// of the HTTP request. Use the `urlRequest(forCalculating:)` method to get an HTTP request that is a GET or POST - /// request as necessary. - /// - /// After requesting the URL returned by this method, you can parse the JSON data in the response and pass it into - /// the ``Route/init(from:)`` initializer. Alternatively, you can use the ``calculate(_:completionHandler:)-8je4q`` - /// method, which automatically sends the request and parses the response. - /// - Parameters: - /// - options: A ``DirectionsOptions`` object specifying the requirements for the resulting routes. - /// - httpMethod: The HTTP method to use. The value of this argument should match the `URLRequest.httpMethod` of - /// the request you send. Currently, only GET and POST requests are supported by the API. - /// - Returns: The URL to send the request to. - open func url(forCalculating options: DirectionsOptions, httpMethod: String) -> URL { - Self.url(forCalculating: options, credentials: credentials, httpMethod: httpMethod) - } - - /// The GET HTTP URL used to fetch the routes from the API. - /// - /// After requesting the URL returned by this method, you can parse the JSON data in the response and pass it into - /// the ``Route/init(from:)`` initializer. Alternatively, you can use the - /// ``calculate(_:completionHandler:)-8je4q`` method, which automatically sends the request and parses the response. - /// - /// - parameter options: A ``DirectionsOptions`` object specifying the requirements for the resulting routes. - /// - parameter credentials: ``Credentials`` data applied to the request. - /// - returns: The URL to send the request to. - public static func url(forCalculating options: DirectionsOptions, credentials: Credentials) -> URL { - return url(forCalculating: options, credentials: credentials, httpMethod: "GET") - } - - /// The HTTP URL used to fetch the routes from the API using the specified HTTP method. - /// - /// The query part of the URL is generally suitable for GET requests. However, if the URL is exceptionally long, it - /// may be more appropriate to send a POST request to a URL without the query part, relegating the query to the body - /// of the HTTP request. Use the `urlRequest(forCalculating:)` method to get an HTTP request that is a GET or POST - /// request as necessary. - /// - /// After requesting the URL returned by this method, you can parse the JSON data in the response and pass it into - /// the ``Route/init(from:)`` initializer. Alternatively, you can use the ``calculate(_:completionHandler:)-8je4q`` - /// method, which automatically sends the request and parses the response. - /// - Parameters: - /// - options: A ``DirectionsOptions`` object specifying the requirements for the resulting routes. - /// - credentials: ``Credentials`` data applied to the request. - /// - httpMethod: The HTTP method to use. The value of this argument should match the `URLRequest.httpMethod` of - /// the request you send. Currently, only GET and POST requests are supported by the API. - /// - Returns: The URL to send the request to. - public static func url( - forCalculating options: DirectionsOptions, - credentials: Credentials, - httpMethod: String - ) -> URL { - let includesQuery = httpMethod != "POST" - var params = (includesQuery ? options.urlQueryItems : []) - params.append(contentsOf: credentials.authenticationParams) - - let unparameterizedURL = URL(path: includesQuery ? options.path : options.abridgedPath, host: credentials.host) - var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! - components.queryItems = params - return components.url! - } - - /// The HTTP request used to fetch the routes from the API. - /// - /// The returned request is a GET or POST request as necessary to accommodate URL length limits. - /// - /// After sending the request returned by this method, you can parse the JSON data in the response and pass it into - /// the ``Route.init(json:waypoints:profileIdentifier:)`` initializer. Alternatively, you can use the - /// `calculate(_:options:)` method, which automatically sends the request and parses the response. - /// - /// - Parameter options: A ``DirectionsOptions`` object specifying the requirements for the resulting routes. - /// - Returns: A GET or POST HTTP request to calculate the specified options. - open func urlRequest(forCalculating options: DirectionsOptions) -> URLRequest { - if options.waypoints.count < 2 { assertionFailure("waypoints array requires at least 2 waypoints") } - let getURL = Self.url(forCalculating: options, credentials: credentials, httpMethod: "GET") - var request = URLRequest(url: getURL) - if getURL.absoluteString.count > MaximumURLLength { - request.url = Self.url(forCalculating: options, credentials: credentials, httpMethod: "POST") - - let body = options.httpBody.data(using: .utf8) - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpMethod = "POST" - request.httpBody = body - } - request.setupUserAgentString() - return request - } -} - -@available(*, unavailable) -extension Directions: @unchecked Sendable {} - -/// Keys to pass to populate a `userInfo` dictionary, which is passed to the `JSONDecoder` upon trying to decode a -/// ``RouteResponse``, ``MapMatchingResponse`` or ``RouteRefreshResponse``. -extension CodingUserInfoKey { - public static let options = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.routeOptions")! - public static let httpResponse = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.httpResponse")! - public static let credentials = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.credentials")! - public static let tracepoints = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.tracepoints")! - - public static let responseIdentifier = - CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.responseIdentifier")! - public static let routeIndex = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.routeIndex")! - public static let startLegIndex = CodingUserInfoKey(rawValue: "com.mapbox.directions.coding.startLegIndex")! -} - -extension Credentials { - fileprivate var authenticationParams: [URLQueryItem] { - var params: [URLQueryItem] = [ - URLQueryItem(name: "access_token", value: accessToken), - ] - - if let skuToken { - params.append(URLQueryItem(name: "sku", value: skuToken)) - } - return params - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/DirectionsError.swift b/ios/Classes/Navigation/MapboxDirections/DirectionsError.swift deleted file mode 100644 index fd27a300d..000000000 --- a/ios/Classes/Navigation/MapboxDirections/DirectionsError.swift +++ /dev/null @@ -1,215 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -/// An error that occurs when calculating directions. -public enum DirectionsError: LocalizedError { - public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { - if let response = response as? HTTPURLResponse { - switch (response.statusCode, code ?? "") { - case (200, "NoRoute"): - self = .unableToRoute - case (200, "NoSegment"): - self = .unableToLocate - case (200, "NoMatch"): - self = .noMatches - case (422, "TooManyCoordinates"): - self = .tooManyCoordinates - case (404, "ProfileNotFound"): - self = .profileNotFound - case (413, _): - self = .requestTooLarge - case (422, "InvalidInput"): - self = .invalidInput(message: message) - case (429, _): - self = .rateLimited( - rateLimitInterval: response.rateLimitInterval, - rateLimit: response.rateLimit, - resetTime: response.rateLimitResetTime - ) - default: - self = .unknown(response: response, underlying: error, code: code, message: message) - } - } else { - self = .unknown(response: response, underlying: error, code: code, message: message) - } - } - - /// There is no network connection available to perform the network request. - case network(_: URLError) - - /// The server returned an empty response. - case noData - - /// The API recieved input that it didn't understand. - case invalidInput(message: String?) - - /// The server returned a response that isn’t correctly formatted. - case invalidResponse(_: URLResponse?) - - /// No route could be found between the specified locations. - /// - /// Make sure it is possible to travel between the locations with the mode of transportation implied by the - /// profileIdentifier option. For example, it is impossible to travel by car from one continent to another without - /// either a land bridge or a ferry connection. - case unableToRoute - - /// The specified coordinates could not be matched to the road network. - /// - /// Try again making sure that your tracepoints lie in close proximity to a road or path. - case noMatches - - /// The request specifies too many coordinates. - /// - /// Try again with fewer coordinates. - case tooManyCoordinates - - /// A specified location could not be associated with a roadway or pathway. - /// - /// Make sure the locations are close enough to a roadway or pathway. Try setting the - /// ``Waypoint/coordinateAccuracy`` property of all the waypoints to `nil`. - case unableToLocate - - /// Unrecognized profile identifier. - /// - /// Make sure the ``DirectionsOptions/profileIdentifier`` option is set to one of the predefined values, such as - /// ``ProfileIdentifier/automobile``. - case profileNotFound - - /// The request is too large. - /// - /// Try specifying fewer waypoints or giving the waypoints shorter names. - case requestTooLarge - - /// Too many requests have been made with the same access token within a certain period of time. - /// - /// Wait before retrying. - case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) - - /// Unknown error case. Look at associated values for more details. - case unknown(response: URLResponse?, underlying: Error?, code: String?, message: String?) - - public var failureReason: String? { - switch self { - case .network: - return "The client does not have a network connection to the server." - case .noData: - return "The server returned an empty response." - case .invalidInput(let message): - return message - case .invalidResponse: - return "The server returned a response that isn’t correctly formatted." - case .unableToRoute: - return "No route could be found between the specified locations." - case .noMatches: - return "The specified coordinates could not be matched to the road network." - case .tooManyCoordinates: - return "The request specifies too many coordinates." - case .unableToLocate: - return "A specified location could not be associated with a roadway or pathway." - case .profileNotFound: - return "Unrecognized profile identifier." - case .requestTooLarge: - return "The request is too large." - case .rateLimited(rateLimitInterval: let interval, rateLimit: let limit, _): - guard let interval, let limit else { - return "Too many requests." - } -#if os(Linux) - let formattedInterval = "\(interval) seconds" -#else - let intervalFormatter = DateComponentsFormatter() - intervalFormatter.unitsStyle = .full - let formattedInterval = intervalFormatter.string(from: interval) ?? "\(interval) seconds" -#endif - let formattedCount = NumberFormatter.localizedString(from: NSNumber(value: limit), number: .decimal) - return "More than \(formattedCount) requests have been made with this access token within a period of \(formattedInterval)." - case .unknown(_, underlying: let error, _, let message): - return message - ?? (error as NSError?)?.userInfo[NSLocalizedFailureReasonErrorKey] as? String - ?? HTTPURLResponse.localizedString(forStatusCode: (error as NSError?)?.code ?? -1) - } - } - - public var recoverySuggestion: String? { - switch self { - case .network(_), .noData, .invalidInput, .invalidResponse: - return nil - case .unableToRoute: - return "Make sure it is possible to travel between the locations with the mode of transportation implied by the profileIdentifier option. For example, it is impossible to travel by car from one continent to another without either a land bridge or a ferry connection." - case .noMatches: - return "Try again making sure that your tracepoints lie in close proximity to a road or path." - case .tooManyCoordinates: - return "Try again with 100 coordinates or fewer." - case .unableToLocate: - return "Make sure the locations are close enough to a roadway or pathway. Try setting the coordinateAccuracy property of all the waypoints to nil." - case .profileNotFound: - return "Make sure the profileIdentifier option is set to one of the provided constants, such as ProfileIdentifier.automobile." - case .requestTooLarge: - return "Try specifying fewer waypoints or giving the waypoints shorter names." - case .rateLimited(rateLimitInterval: _, rateLimit: _, resetTime: let rolloverTime): - guard let rolloverTime else { - return nil - } - let formattedDate: String = DateFormatter.localizedString( - from: rolloverTime, - dateStyle: .long, - timeStyle: .long - ) - return "Wait until \(formattedDate) before retrying." - case .unknown(_, underlying: let error, _, _): - return (error as NSError?)?.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String - } - } -} - -extension DirectionsError: Equatable { - public static func == (lhs: DirectionsError, rhs: DirectionsError) -> Bool { - switch (lhs, rhs) { - case (.noData, .noData), - (.unableToRoute, .unableToRoute), - (.noMatches, .noMatches), - (.tooManyCoordinates, .tooManyCoordinates), - (.unableToLocate, .unableToLocate), - (.profileNotFound, .profileNotFound), - (.requestTooLarge, .requestTooLarge): - return true - case (.network(let lhsError), .network(let rhsError)): - return lhsError == rhsError - case (.invalidResponse(let lhsResponse), .invalidResponse(let rhsResponse)): - return lhsResponse == rhsResponse - case (.invalidInput(let lhsMessage), .invalidInput(let rhsMessage)): - return lhsMessage == rhsMessage - case ( - .rateLimited(let lhsRateLimitInterval, let lhsRateLimit, let lhsResetTime), - .rateLimited(let rhsRateLimitInterval, let rhsRateLimit, let rhsResetTime) - ): - return lhsRateLimitInterval == rhsRateLimitInterval - && lhsRateLimit == rhsRateLimit - && lhsResetTime == rhsResetTime - case ( - .unknown(let lhsResponse, let lhsUnderlying, let lhsCode, let lhsMessage), - .unknown(let rhsResponse, let rhsUnderlying, let rhsCode, let rhsMessage) - ): - return lhsResponse == rhsResponse - && type(of: lhsUnderlying) == type(of: rhsUnderlying) - && lhsUnderlying?.localizedDescription == rhsUnderlying?.localizedDescription - && lhsCode == rhsCode - && lhsMessage == rhsMessage - default: - return false - } - } -} - -/// An error that occurs when encoding or decoding a type defined by the MapboxDirections framework. -public enum DirectionsCodingError: Error { - /// Decoding this type requires the `Decoder.userInfo` dictionary to contain the ``Swift/CodingUserInfoKey/options`` - /// key. - case missingOptions - - /// Decoding this type requires the `Decoder.userInfo` dictionary to contain the - /// ``Swift/CodingUserInfoKey/credentials`` key. - case missingCredentials -} diff --git a/ios/Classes/Navigation/MapboxDirections/DirectionsOptions.swift b/ios/Classes/Navigation/MapboxDirections/DirectionsOptions.swift deleted file mode 100644 index b95f23505..000000000 --- a/ios/Classes/Navigation/MapboxDirections/DirectionsOptions.swift +++ /dev/null @@ -1,610 +0,0 @@ -import Foundation -import Turf - -/// Maximum length of an HTTP request URL for the purposes of switching from GET to POST. -/// -/// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-general -let MaximumURLLength = 1024 * 8 - -/// A ``RouteShapeFormat`` indicates the format of a route or match shape in the raw HTTP response. -public enum RouteShapeFormat: String, Codable, Equatable, Sendable { - /// The route’s shape is delivered in [GeoJSON](http://geojson.org/) format. - /// - /// This standard format is human-readable and can be parsed straightforwardly, but it is far more verbose than - /// ``RouteShapeFormat/polyline``. - case geoJSON = "geojson" - /// The route’s shape is delivered in [encoded polyline - /// algorithm](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) format with - /// 1×10−5 precision. - /// - /// This machine-readable format is considerably more compact than ``RouteShapeFormat/geoJSON`` but less precise - /// than ``RouteShapeFormat/polyline6``. - case polyline - /// The route’s shape is delivered in [encoded polyline - /// algorithm](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) format with - /// 1×10−6 precision. - /// - /// This format is an order of magnitude more precise than ``RouteShapeFormat/polyline``. - case polyline6 - - static let `default` = RouteShapeFormat.polyline -} - -/// A ``RouteShapeResolution`` indicates the level of detail in a route’s shape, or whether the shape is present at all. -public enum RouteShapeResolution: String, Codable, Equatable, Sendable { - /// The route’s shape is omitted. - /// - /// Specify this resolution if you do not intend to show the route line to the user or analyze the route line in any - /// way. - case none = "false" - /// The route’s shape is simplified. - /// - /// This resolution considerably reduces the size of the response. The resulting shape is suitable for display at a - /// low zoom level, but it lacks the detail necessary for focusing on individual segments of the route. - case low = "simplified" - /// The route’s shape is as detailed as possible. - /// - /// The resulting shape is equivalent to concatenating the shapes of all the route’s consitituent steps. You can - /// focus on individual segments of this route while faithfully representing the path of the route. If you only - /// intend to show a route overview and do not need to analyze the route line in any way, consider specifying - /// ``RouteShapeResolution/low`` instead to considerably reduce the size of the response. - case full -} - -/// A system of units of measuring distances and other quantities. -public enum MeasurementSystem: String, Codable, Equatable, Sendable { - /// U.S. customary and British imperial units. - /// - /// Distances are measured in miles and feet. - case imperial - - /// The metric system. - /// - /// Distances are measured in kilometers and meters. - case metric -} - -@available(*, deprecated, renamed: "DirectionsPriority") -public typealias MBDirectionsPriority = DirectionsPriority - -/// A number that influences whether a route should prefer or avoid roadways or pathways of a given type. -public struct DirectionsPriority: Hashable, RawRepresentable, Codable, Equatable, Sendable { - public init(rawValue: Double) { - self.rawValue = rawValue - } - - public var rawValue: Double - - /// The priority level with which a route avoids a particular type of roadway or pathway. - public static let low = DirectionsPriority(rawValue: -1.0) - - /// The priority level with which a route neither avoids nor prefers a particular type of roadway or pathway. - public static let medium = DirectionsPriority(rawValue: 0.0) - - /// The priority level with which a route prefers a particular type of roadway or pathway. - public static let high = DirectionsPriority(rawValue: 1.0) -} - -/// Options for calculating results from the Mapbox Directions service. -/// -/// You do not create instances of this class directly. Instead, create instances of ``MatchOptions`` or -/// ``RouteOptions``. -open class DirectionsOptions: Codable, @unchecked Sendable { - // MARK: Creating a Directions Options Object - - /// Initializes an options object for routes between the given waypoints and an optional profile identifier. - /// - /// Do not call ``DirectionsOptions/init(waypoints:profileIdentifier:queryItems:)`` directly; instead call the - /// corresponding - /// initializer of ``RouteOptions`` or ``MatchOptions``. - /// - Parameters: - /// - waypoints: An array of ``Waypoint`` objects representing locations that the route should visit in - /// chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 - /// waypoints. (Some profiles, such as ``ProfileIdentifier/automobileAvoidingTraffic``, [may have lower - /// limits](https://docs.mapbox.com/api/navigation/#directions).) - /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. - /// ``ProfileIdentifier/automobile`` is used by default. - /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. - public required init( - waypoints: [Waypoint], - profileIdentifier: ProfileIdentifier? = nil, - queryItems: [URLQueryItem]? = nil - ) { - var waypoints = waypoints - self.profileIdentifier = profileIdentifier ?? .automobile - - guard let queryItems else { - self.waypoints = waypoints - return - } - - let mappedQueryItems = [String: String]( - queryItems.compactMap { - guard let value = $0.value else { return nil } - return ($0.name, value) - }, - uniquingKeysWith: { _, latestValue in - return latestValue - } - ) - - if let mappedValue = mappedQueryItems[CodingKeys.shapeFormat.stringValue], - let shapeFormat = RouteShapeFormat(rawValue: mappedValue) - { - self.shapeFormat = shapeFormat - } - if let mappedValue = mappedQueryItems[CodingKeys.routeShapeResolution.stringValue], - let routeShapeResolution = RouteShapeResolution(rawValue: mappedValue) - { - self.routeShapeResolution = routeShapeResolution - } - if mappedQueryItems[CodingKeys.includesSteps.stringValue] == "true" { - self.includesSteps = true - } - if let mappedValue = mappedQueryItems[CodingKeys.locale.stringValue] { - self.locale = Locale(identifier: mappedValue) - } - if mappedQueryItems[CodingKeys.includesSpokenInstructions.stringValue] == "true" { - self.includesSpokenInstructions = true - } - if let mappedValue = mappedQueryItems[CodingKeys.distanceMeasurementSystem.stringValue], - let measurementSystem = MeasurementSystem(rawValue: mappedValue) - { - self.distanceMeasurementSystem = measurementSystem - } - if mappedQueryItems[CodingKeys.includesVisualInstructions.stringValue] == "true" { - self.includesVisualInstructions = true - } - if let mappedValue = mappedQueryItems[CodingKeys.attributeOptions.stringValue], - let attributeOptions = AttributeOptions(descriptions: mappedValue.components(separatedBy: ",")) - { - self.attributeOptions = attributeOptions - } - if let mappedValue = mappedQueryItems["waypoints"] { - let indicies = mappedValue.components(separatedBy: ";").compactMap { Int($0) } - if !indicies.isEmpty { - for index in waypoints.indices { - waypoints[index].separatesLegs = indicies.contains(index) - } - } - } - - let waypointsData = [ - mappedQueryItems["approaches"]?.components(separatedBy: ";"), - mappedQueryItems["bearings"]?.components(separatedBy: ";"), - mappedQueryItems["radiuses"]?.components(separatedBy: ";"), - mappedQueryItems["waypoint_names"]?.components(separatedBy: ";"), - mappedQueryItems["snapping_include_closures"]?.components(separatedBy: ";"), - mappedQueryItems["snapping_include_static_closures"]?.components(separatedBy: ";"), - ] as [[String]?] - - let getElement: ((_ array: [String]?, _ index: Int) -> String?) = { array, index in - if array?.count ?? -1 > index { - return array?[index] - } - return nil - } - - for waypointIndex in waypoints.indices { - if let approach = getElement(waypointsData[0], waypointIndex) { - waypoints[waypointIndex].allowsArrivingOnOppositeSide = approach == "unrestricted" ? true : false - } - - if let descriptions = getElement(waypointsData[1], waypointIndex)?.components(separatedBy: ",") { - waypoints[waypointIndex].heading = LocationDirection(descriptions.first!) - waypoints[waypointIndex].headingAccuracy = LocationDirection(descriptions.last!) - } - - if let accuracy = getElement(waypointsData[2], waypointIndex) { - waypoints[waypointIndex].coordinateAccuracy = LocationAccuracy(accuracy) - } - - if let snaps = getElement(waypointsData[4], waypointIndex) { - waypoints[waypointIndex].allowsSnappingToClosedRoad = snaps == "true" - } - - if let snapsToStaticallyClosed = getElement(waypointsData[5], waypointIndex) { - waypoints[waypointIndex].allowsSnappingToStaticallyClosedRoad = snapsToStaticallyClosed == "true" - } - } - - var separatesLegIndex = 0 - for waypointIndex in waypoints.indices { - guard waypoints[waypointIndex].separatesLegs else { continue } - - if let name = getElement(waypointsData[3], separatesLegIndex) { - waypoints[waypointIndex].name = name - } - separatesLegIndex += 1 - } - - self.waypoints = waypoints - } - - /// Creates new options object by deserializing given `url` - /// - /// Initialization fails if it is unable to extract ``waypoints`` list and ``profileIdentifier``. If other - /// properties are failed to decode - it will just skip them. - /// - Parameter url: An URL, used to make a route request. - public convenience init?(url: URL) { - guard url.pathComponents.count >= 3 else { - return nil - } - - let waypointsString = url.lastPathComponent.replacingOccurrences(of: ".json", with: "") - let waypoints: [Waypoint] = waypointsString.components(separatedBy: ";").compactMap { - let coordinates = $0.components(separatedBy: ",") - guard coordinates.count == 2, - let latitudeString = coordinates.last, - let longitudeString = coordinates.first, - let latitude = LocationDegrees(latitudeString), - let longitude = LocationDegrees(longitudeString) - else { - return nil - } - return Waypoint(coordinate: .init( - latitude: latitude, - longitude: longitude - )) - } - - guard waypoints.count >= 2 else { - return nil - } - - let profileIdentifier = ProfileIdentifier( - rawValue: url.pathComponents.dropLast().suffix(2) - .joined(separator: "/") - ) - - self.init( - waypoints: waypoints, - profileIdentifier: profileIdentifier, - queryItems: URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems - ) - - // Distinguish between Directions API and Map Matching API URLs. - guard url.pathComponents.dropLast().joined(separator: "/").hasSuffix(abridgedPath) else { - return nil - } - } - - private enum CodingKeys: String, CodingKey { - case waypoints - case profileIdentifier = "profile" - case includesSteps = "steps" - case shapeFormat = "geometries" - case routeShapeResolution = "overview" - case attributeOptions = "annotations" - case locale = "language" - case includesSpokenInstructions = "voice_instructions" - case distanceMeasurementSystem = "voice_units" - case includesVisualInstructions = "banner_instructions" - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(waypoints, forKey: .waypoints) - try container.encode(profileIdentifier, forKey: .profileIdentifier) - try container.encode(includesSteps, forKey: .includesSteps) - try container.encode(shapeFormat, forKey: .shapeFormat) - try container.encode(routeShapeResolution, forKey: .routeShapeResolution) - try container.encode(attributeOptions, forKey: .attributeOptions) - try container.encode(locale.identifier, forKey: .locale) - try container.encode(includesSpokenInstructions, forKey: .includesSpokenInstructions) - try container.encode(distanceMeasurementSystem, forKey: .distanceMeasurementSystem) - try container.encode(includesVisualInstructions, forKey: .includesVisualInstructions) - } - - public required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.waypoints = try container.decode([Waypoint].self, forKey: .waypoints) - self.profileIdentifier = try container.decode(ProfileIdentifier.self, forKey: .profileIdentifier) - self.includesSteps = try container.decode(Bool.self, forKey: .includesSteps) - self.shapeFormat = try container.decode(RouteShapeFormat.self, forKey: .shapeFormat) - self.routeShapeResolution = try container.decode(RouteShapeResolution.self, forKey: .routeShapeResolution) - self.attributeOptions = try container.decode(AttributeOptions.self, forKey: .attributeOptions) - let identifier = try container.decode(String.self, forKey: .locale) - self.locale = Locale(identifier: identifier) - self.includesSpokenInstructions = try container.decode(Bool.self, forKey: .includesSpokenInstructions) - self.distanceMeasurementSystem = try container.decode( - MeasurementSystem.self, - forKey: .distanceMeasurementSystem - ) - self.includesVisualInstructions = try container.decode(Bool.self, forKey: .includesVisualInstructions) - } - - // MARK: Specifying the Path of the Route - - /// An array of ``Waypoint`` objects representing locations that the route should visit in chronological order. - /// - /// A waypoint object indicates a location to visit, as well as an optional heading from which to approach the - /// location. - /// The array should contain at least two waypoints(the source and destination) and at most 25 waypoints. - public var waypoints: [Waypoint] - - /// The waypoints that separate legs. - var legSeparators: [Waypoint] { - var waypoints = waypoints - guard waypoints.count > 1 else { return [] } - - let source = waypoints.removeFirst() - let destination = waypoints.removeLast() - return [source] + waypoints.filter(\.separatesLegs) + [destination] - } - - // MARK: Specifying the Mode of Transportation - - /// A string specifying the primary mode of transportation for the routes. - /// - /// The default value of this property is ``ProfileIdentifier/automobile``, which specifies driving directions. - public var profileIdentifier: ProfileIdentifier - - // MARK: Specifying the Response Format - - /// A Boolean value indicating whether ``RouteStep`` objects should be included in the response. - /// - /// If the value of this property is `true`, the returned route contains turn-by-turn instructions. Each returned - /// ``Route`` object contains one or more ``RouteLeg`` object that in turn contains one or more ``RouteStep`` - /// objects. On the other hand, if the value of this property is `false`, the ``RouteLeg`` objects contain no - /// ``RouteStep`` objects. - /// - /// If you only want to know the distance or estimated travel time to a destination, set this property to `false` to - /// minimize the size of the response and the time it takes to calculate the response. If you need to display - /// turn-by-turn instructions, set this property to `true`. - /// - /// The default value of this property is `false`. - public var includesSteps = false - - /// Format of the data from which the shapes of the returned route and its steps are derived. - /// - /// This property has no effect on the returned shape objects, although the choice of format can significantly - /// affect the size of the underlying HTTP response. - /// - /// The default value of this property is ``RouteShapeFormat/polyline``. - public var shapeFormat = RouteShapeFormat.polyline - - /// Resolution of the shape of the returned route. - /// - /// This property has no effect on the shape of the returned route’s steps. - /// - /// The default value of this property is ``RouteShapeResolution/low``, specifying a low-resolution route shape. - public var routeShapeResolution = RouteShapeResolution.low - - /// AttributeOptions for the route. Any combination of ``AttributeOptions`` can be specified. - /// - /// By default, no attribute options are specified. It is recommended that ``routeShapeResolution`` be set to - /// ``RouteShapeResolution/full``. - public var attributeOptions: AttributeOptions = [] - - /// The locale in which the route’s instructions are written. - /// - /// If you use the MapboxDirections framework with the Mapbox Directions API or Map Matching API, this property - /// affects the sentence contained within the ``RouteStep/instructions`` property, but it does not affect any road - /// names contained in that property or other properties such as ``RouteStep/names``. - /// - /// The Directions API can provide instructions in [a number of - /// languages](https://docs.mapbox.com/api/navigation/#instructions-languages). Set this property to - /// `Bundle.main.preferredLocalizations.first` or `Locale.autoupdatingCurrent` to match the application’s language - /// or the system language, respectively. - /// - /// By default, this property is set to the current system locale. - public var locale = Locale.current { - didSet { - distanceMeasurementSystem = locale.usesMetricSystem ? .metric : .imperial - } - } - - /// A Boolean value indicating whether each route step includes an array of ``SpokenInstruction``. - /// - /// If this option is set to true, the ``RouteStep/instructionsSpokenAlongStep`` property is set to an array of - /// ``SpokenInstruction``. - public var includesSpokenInstructions = false - - /// The measurement system used in spoken instructions included in route steps. - /// - /// If the ``includesSpokenInstructions`` property is set to `true`, this property determines the units used for - /// measuring the distance remaining until an upcoming maneuver. If the ``includesSpokenInstructions`` property is - /// set to `false`, this property has no effect. - /// - /// You should choose a measurement system appropriate for the current region. You can also allow the user to - /// indicate their preferred measurement system via a setting. - public var distanceMeasurementSystem: MeasurementSystem = Locale.current.usesMetricSystem ? .metric : .imperial - - /// If true, each ``RouteStep`` will contain the property ``RouteStep/instructionsDisplayedAlongStep``. - /// - /// ``RouteStep/instructionsDisplayedAlongStep`` contains an array of ``VisualInstruction`` objects used for - /// visually conveying - /// information about a given ``RouteStep``. - public var includesVisualInstructions = false - - /// The time immediately before a `Directions` object fetched this result. - /// - /// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to - /// `nil`; use the `URLSessionTaskTransactionMetrics.fetchStartDate` property instead. This property may also be set - /// to `nil` if you create this result from a JSON object or encoded object. - /// - /// This property does not persist after encoding and decoding. - public var fetchStartDate: Date? - - // MARK: Getting the Request URL - - /// The path of the request URL, specifying service name, version and profile. - /// - /// The query items are included in the URL of a GET request or the body of a POST request. - var abridgedPath: String { - assertionFailure("abridgedPath should be overriden by subclass") - return "" - } - - /// The path of the request URL, not including the hostname or any parameters. - var path: String { - guard let coordinates else { - assertionFailure("No query") - return "" - } - - if waypoints.count < 2 { - return "\(abridgedPath)" - } - - return "\(abridgedPath)/\(coordinates)" - } - - /// An array of URL query items (parameters) to include in an HTTP request. - /// - /// The query items are included in the URL of a GET request or the body of a POST request. - public var urlQueryItems: [URLQueryItem] { - var queryItems: [URLQueryItem] = [ - URLQueryItem(name: "geometries", value: shapeFormat.rawValue), - URLQueryItem(name: "overview", value: routeShapeResolution.rawValue), - - URLQueryItem(name: "steps", value: String(includesSteps)), - URLQueryItem(name: "language", value: locale.identifier), - ] - - let mustArriveOnDrivingSide = !waypoints.filter { !$0.allowsArrivingOnOppositeSide }.isEmpty - if mustArriveOnDrivingSide { - let approaches = waypoints.map { $0.allowsArrivingOnOppositeSide ? "unrestricted" : "curb" } - queryItems.append(URLQueryItem(name: "approaches", value: approaches.joined(separator: ";"))) - } - - if includesSpokenInstructions { - queryItems.append(URLQueryItem(name: "voice_instructions", value: String(includesSpokenInstructions))) - queryItems.append(URLQueryItem(name: "voice_units", value: distanceMeasurementSystem.rawValue)) - } - - if includesVisualInstructions { - queryItems.append(URLQueryItem(name: "banner_instructions", value: String(includesVisualInstructions))) - } - - // Include headings and heading accuracies if any waypoint has a nonnegative heading. - if let bearings { - queryItems.append(URLQueryItem(name: "bearings", value: bearings)) - } - - // Include location accuracies if any waypoint has a nonnegative coordinate accuracy. - if let radiuses { - queryItems.append(URLQueryItem(name: "radiuses", value: radiuses)) - } - - if let annotations { - queryItems.append(URLQueryItem(name: "annotations", value: annotations)) - } - - if let waypointIndices { - queryItems.append(URLQueryItem(name: "waypoints", value: waypointIndices)) - } - - if let names = waypointNames { - queryItems.append(URLQueryItem(name: "waypoint_names", value: names)) - } - - if let snapping = closureSnapping { - queryItems.append(URLQueryItem(name: "snapping_include_closures", value: snapping)) - } - - if let staticClosureSnapping { - queryItems.append(URLQueryItem(name: "snapping_include_static_closures", value: staticClosureSnapping)) - } - - return queryItems - } - - var bearings: String? { - guard waypoints.contains(where: { $0.heading ?? -1 >= 0 }) else { - return nil - } - return waypoints.map(\.headingDescription).joined(separator: ";") - } - - var radiuses: String? { - guard waypoints.contains(where: { $0.coordinateAccuracy ?? -1 >= 0 }) else { - return nil - } - - let accuracies = waypoints.map { waypoint -> String in - guard let accuracy = waypoint.coordinateAccuracy, accuracy >= 0 else { - return "unlimited" - } - return String(accuracy) - } - return accuracies.joined(separator: ";") - } - - private var approaches: String? { - if waypoints.filter({ !$0.allowsArrivingOnOppositeSide }).isEmpty { - return nil - } - return waypoints.map { $0.allowsArrivingOnOppositeSide ? "unrestricted" : "curb" }.joined(separator: ";") - } - - private var annotations: String? { - if attributeOptions.isEmpty { - return nil - } - return attributeOptions.description - } - - private var waypointIndices: String? { - var waypointIndices = waypoints.indices { $0.separatesLegs } - waypointIndices.insert(waypoints.startIndex) - waypointIndices.insert(waypoints.endIndex - 1) - - guard waypointIndices.count < waypoints.count else { - return nil - } - return waypointIndices.map(String.init(describing:)).joined(separator: ";") - } - - private var waypointNames: String? { - guard !waypoints.compactMap(\.name).isEmpty, waypoints.count > 1 else { - return nil - } - return legSeparators.map { $0.name ?? "" }.joined(separator: ";") - } - - var coordinates: String? { - return waypoints.map(\.coordinate.requestDescription).joined(separator: ";") - } - - var closureSnapping: String? { - makeStringFromBoolProperties(of: waypoints, for: \.allowsSnappingToClosedRoad) - } - - var staticClosureSnapping: String? { - makeStringFromBoolProperties(of: waypoints, for: \.allowsSnappingToStaticallyClosedRoad) - } - - private func makeStringFromBoolProperties(of elements: [T], for keyPath: KeyPath) -> String? { - guard elements.contains(where: { $0[keyPath: keyPath] }) else { return nil } - return elements.map { $0[keyPath: keyPath] ? "true" : "" }.joined(separator: ";") - } - - var httpBody: String { - guard let coordinates else { return "" } - var components = URLComponents() - components.queryItems = urlQueryItems + [ - URLQueryItem(name: "coordinates", value: coordinates), - ] - return components.percentEncodedQuery ?? "" - } -} - -extension DirectionsOptions: Equatable { - public static func == (lhs: DirectionsOptions, rhs: DirectionsOptions) -> Bool { - return lhs.waypoints == rhs.waypoints && - lhs.profileIdentifier == rhs.profileIdentifier && - lhs.includesSteps == rhs.includesSteps && - lhs.shapeFormat == rhs.shapeFormat && - lhs.routeShapeResolution == rhs.routeShapeResolution && - lhs.attributeOptions == rhs.attributeOptions && - lhs.locale.identifier == rhs.locale.identifier && - lhs.includesSpokenInstructions == rhs.includesSpokenInstructions && - lhs.distanceMeasurementSystem == rhs.distanceMeasurementSystem && - lhs.includesVisualInstructions == rhs.includesVisualInstructions - } -} - -@available(*, unavailable) -extension DirectionsOptions: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxDirections/DirectionsResult.swift b/ios/Classes/Navigation/MapboxDirections/DirectionsResult.swift deleted file mode 100644 index 2ef311f70..000000000 --- a/ios/Classes/Navigation/MapboxDirections/DirectionsResult.swift +++ /dev/null @@ -1,245 +0,0 @@ -import Foundation -import Turf - -public enum DirectionsResultCodingKeys: String, CodingKey, CaseIterable { - case shape = "geometry" - case legs - case distance - case expectedTravelTime = "duration" - case typicalTravelTime = "duration_typical" - case directionsOptions - case speechLocale = "voiceLocale" -} - -public struct DirectionsCodingKey: CodingKey { - public var intValue: Int? { nil } - public init?(intValue: Int) { - nil - } - - public let stringValue: String - public init(stringValue: String) { - self.stringValue = stringValue - } - - public static func directionsResult(_ key: DirectionsResultCodingKeys) -> Self { - .init(stringValue: key.rawValue) - } -} - -/// A `DirectionsResult` represents a result returned from either the Mapbox Directions service. -/// -/// You do not create instances of this class directly. Instead, you receive ``Route`` or ``Match`` objects when you -/// request directions using the `Directions.calculate(_:completionHandler:)` or -/// `Directions.calculateRoutes(matching:completionHandler:)` method. -public protocol DirectionsResult: Codable, ForeignMemberContainer, Equatable, Sendable { - // MARK: Getting the Shape of the Route - - /// The roads or paths taken as a contiguous polyline. - /// - /// The shape may be `nil` or simplified depending on the ``DirectionsOptions/routeShapeResolution`` property of the - /// original ``RouteOptions`` or ``MatchOptions`` object. - /// - /// Using the [Mapbox Maps SDK for iOS](https://docs.mapbox.com/ios/maps/) or [Mapbox Maps SDK for - /// macOS](https://mapbox.github.io/mapbox-gl-native/macos/), you can create an `MGLPolyline` object using these - /// coordinates to display an overview of the route on an `MGLMapView`. - var shape: LineString? { get } - - // MARK: Getting the Legs Along the Route - - /// The legs that are traversed in order. - /// - /// The number of legs in this array depends on the number of waypoints. A route with two waypoints (the source and - /// destination) has one leg, a route with three waypoints (the source, an intermediate waypoint, and the - /// destination) has two legs, and so on. - /// - /// To determine the name of the route, concatenate the names of the route’s legs. - var legs: [RouteLeg] { get set } - - // MARK: Getting Statistics About the Route - - /// The route’s distance, measured in meters. - /// - /// The value of this property accounts for the distance that the user must travel to traverse the path of the - /// route. It is the sum of the ``RouteLeg/distance`` properties of the route’s legs, not the sum of the direct - /// distances between the route’s waypoints. You should not assume that the user would travel along this distance at - /// a fixed speed. - var distance: Turf.LocationDistance { get } - - /// The route’s expected travel time, measured in seconds. - /// - /// The value of this property reflects the time it takes to traverse the entire route. It is the sum of the - /// ``expectedTravelTime`` properties of the route’s legs. If the route was calculated using the - /// ``ProfileIdentifier/automobileAvoidingTraffic`` profile, this property reflects current traffic conditions at - /// the time of the request, not necessarily the traffic conditions at the time the user would begin the route. For - /// other profiles, this property reflects travel time under ideal conditions and does not account for traffic - /// congestion. If the route makes use of a ferry or train, the actual travel time may additionally be subject to - /// the schedules of those services. - /// - /// Do not assume that the user would travel along the route at a fixed speed. For more granular travel times, use - /// the ``RouteLeg/expectedTravelTime`` or ``RouteStep/expectedTravelTime``. For even more granularity, specify the - /// ``AttributeOptions/expectedTravelTime`` option and use the ``RouteLeg/expectedSegmentTravelTimes`` property. - var expectedTravelTime: TimeInterval { get set } - - /// The route’s typical travel time, measured in seconds. - /// - /// The value of this property reflects the typical time it takes to traverse the entire route. It is the sum of the - /// ``typicalTravelTime`` properties of the route’s legs. This property is available when using the - /// ``ProfileIdentifier/automobileAvoidingTraffic`` profile. This property reflects typical traffic conditions at - /// the time of the request, not necessarily the typical traffic conditions at the time the user would begin the - /// route. If the route makes use of a ferry, the typical travel time may additionally be subject to the schedule of - /// this service. - /// - /// Do not assume that the user would travel along the route at a fixed speed. For more granular typical travel - /// times, use the ``RouteLeg/typicalTravelTime`` or ``RouteStep/typicalTravelTime``. - var typicalTravelTime: TimeInterval? { get set } - - // MARK: Configuring Speech Synthesis - - /// The locale to use for spoken instructions. - /// - /// This locale is specific to Mapbox Voice API. If `nil` is returned, the instruction should be spoken with an - /// alternative speech synthesizer. - var speechLocale: Locale? { get set } - - // MARK: Auditing the Server Response - - /// The time immediately before a `Directions` object fetched this result. - /// - /// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to - /// `nil`; use the `URLSessionTaskTransactionMetrics.fetchStartDate` property instead. This property may also be set - /// to `nil` if you create this result from a JSON object or encoded object. - /// - /// This property does not persist after encoding and decoding. - var fetchStartDate: Date? { get set } - - /// The time immediately before a `Directions` object received the last byte of this result. - /// - /// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to - /// `nil`; use the `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be - /// set to `nil` if you create this result from a JSON object or encoded object. - /// - /// This property does not persist after encoding and decoding. - var responseEndDate: Date? { get set } - - /// Internal indicator of whether response contained the ``speechLocale`` entry. - /// - /// Directions API includes ``speechLocale`` if ``DirectionsOptions/includesSpokenInstructions`` option was enabled - /// in the request. - /// - /// This property persists after encoding and decoding. - var responseContainsSpeechLocale: Bool { get } - - var legSeparators: [Waypoint?] { get set } -} - -extension DirectionsResult { - public var legSeparators: [Waypoint?] { - get { - return legs.isEmpty ? [] : ([legs[0].source] + legs.map(\.destination)) - } - set { - let endpointsByLeg = zip(newValue, newValue.suffix(from: 1)) - var legIdx = legs.startIndex - for endpoint in endpointsByLeg where legIdx != legs.endIndex { - legs[legIdx].source = endpoint.0 - legs[legIdx].destination = endpoint.1 - legIdx = legs.index(after: legIdx) - } - } - } - - // MARK: - Decode - - static func decodeLegs( - using container: KeyedDecodingContainer, - options: DirectionsOptions - ) throws -> [RouteLeg] { - var legs = try container.decode([RouteLeg].self, forKey: .directionsResult(.legs)) - legs.populate(waypoints: options.legSeparators) - return legs - } - - static func decodeDistance( - using container: KeyedDecodingContainer - ) throws -> Turf.LocationDistance { - try container.decode(Turf.LocationDistance.self, forKey: .directionsResult(.distance)) - } - - static func decodeExpectedTravelTime( - using container: KeyedDecodingContainer - ) throws -> TimeInterval { - try container.decode(TimeInterval.self, forKey: .directionsResult(.expectedTravelTime)) - } - - static func decodeTypicalTravelTime( - using container: KeyedDecodingContainer - ) throws -> TimeInterval? { - try container.decodeIfPresent(TimeInterval.self, forKey: .directionsResult(.typicalTravelTime)) - } - - static func decodeShape( - using container: KeyedDecodingContainer - ) throws -> LineString? { - try container.decodeIfPresent(PolyLineString.self, forKey: .directionsResult(.shape)) - .map(LineString.init(polyLineString:)) - } - - static func decodeSpeechLocale( - using container: KeyedDecodingContainer - ) throws -> Locale? { - try container.decodeIfPresent(String.self, forKey: .directionsResult(.speechLocale)) - .map(Locale.init(identifier:)) - } - - static func decodeResponseContainsSpeechLocale( - using container: KeyedDecodingContainer - ) throws -> Bool { - container.contains(.directionsResult(.speechLocale)) - } - - // MARK: - Encode - - func encodeLegs( - into container: inout KeyedEncodingContainer - ) throws { - try container.encode(legs, forKey: .directionsResult(.legs)) - } - - func encodeShape( - into container: inout KeyedEncodingContainer, - options: DirectionsOptions? - ) throws { - guard let shape else { return } - - let shapeFormat = options?.shapeFormat ?? .default - let polyLineString = PolyLineString(lineString: shape, shapeFormat: shapeFormat) - try container.encode(polyLineString, forKey: .directionsResult(.shape)) - } - - func encodeDistance( - into container: inout KeyedEncodingContainer - ) throws { - try container.encode(distance, forKey: .directionsResult(.distance)) - } - - func encodeExpectedTravelTime( - into container: inout KeyedEncodingContainer - ) throws { - try container.encode(expectedTravelTime, forKey: .directionsResult(.expectedTravelTime)) - } - - func encodeTypicalTravelTime( - into container: inout KeyedEncodingContainer - ) throws { - try container.encodeIfPresent(typicalTravelTime, forKey: .directionsResult(.typicalTravelTime)) - } - - func encodeSpeechLocale( - into container: inout KeyedEncodingContainer - ) throws { - if responseContainsSpeechLocale { - try container.encode(speechLocale?.identifier, forKey: .directionsResult(.speechLocale)) - } - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/DrivingSide.swift b/ios/Classes/Navigation/MapboxDirections/DrivingSide.swift deleted file mode 100644 index 8b42bf5a0..000000000 --- a/ios/Classes/Navigation/MapboxDirections/DrivingSide.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation - -/// A `DrivingSide` indicates which side of the road cars and traffic flow. -public enum DrivingSide: String, Codable, Equatable, Sendable { - /// Indicates driving occurs on the `left` side. - case left - - /// Indicates driving occurs on the `right` side. - case right - - static let `default` = DrivingSide.right -} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/Array.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/Array.swift deleted file mode 100644 index 0cc1a8476..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Extensions/Array.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -extension Collection { - /// Returns an index set containing the indices that satisfy the given predicate. - func indices(where predicate: (Element) throws -> Bool) rethrows -> IndexSet { - return try IndexSet(enumerated().filter { try predicate($0.element) }.map(\.offset)) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/Codable.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/Codable.swift deleted file mode 100644 index cbfe34e5c..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Extensions/Codable.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation -import Turf - -extension LineString { - /// Returns a string representation of the line string in [Polyline Algorithm - /// Format](https://developers.google.com/maps/documentation/utilities/polylinealgorithm). - func polylineEncodedString(precision: Double = 1e5) -> String { -#if canImport(CoreLocation) - let coordinates = coordinates -#else - let coordinates = self.coordinates.map { Polyline.LocationCoordinate2D( - latitude: $0.latitude, - longitude: $0.longitude - ) } -#endif - return encodeCoordinates(coordinates, precision: precision) - } -} - -enum PolyLineString { - case lineString(_ lineString: LineString) - case polyline(_ encodedPolyline: String, precision: Double) - - init(lineString: LineString, shapeFormat: RouteShapeFormat) { - switch shapeFormat { - case .geoJSON: - self = .lineString(lineString) - case .polyline, .polyline6: - let precision = shapeFormat == .polyline6 ? 1e6 : 1e5 - let encodedPolyline = lineString.polylineEncodedString(precision: precision) - self = .polyline(encodedPolyline, precision: precision) - } - } -} - -extension PolyLineString: Codable { - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let options = decoder.userInfo[.options] as? DirectionsOptions - switch options?.shapeFormat ?? .default { - case .geoJSON: - self = try .lineString(container.decode(LineString.self)) - case .polyline, .polyline6: - let precision = options?.shapeFormat == .polyline6 ? 1e6 : 1e5 - let encodedPolyline = try container.decode(String.self) - self = .polyline(encodedPolyline, precision: precision) - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .lineString(let lineString): - try container.encode(lineString) - case .polyline(let encodedPolyline, precision: _): - try container.encode(encodedPolyline) - } - } -} - -struct LocationCoordinate2DCodable: Codable { - var latitude: Turf.LocationDegrees - var longitude: Turf.LocationDegrees - var decodedCoordinates: Turf.LocationCoordinate2D { - return Turf.LocationCoordinate2D(latitude: latitude, longitude: longitude) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() - try container.encode(longitude) - try container.encode(latitude) - } - - init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - self.longitude = try container.decode(Turf.LocationDegrees.self) - self.latitude = try container.decode(Turf.LocationDegrees.self) - } - - init(_ coordinate: Turf.LocationCoordinate2D) { - self.latitude = coordinate.latitude - self.longitude = coordinate.longitude - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/CoreLocation.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/CoreLocation.swift deleted file mode 100644 index 6e9e7c322..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Extensions/CoreLocation.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -#if canImport(CoreLocation) -import CoreLocation -#endif -import Turf - -#if canImport(CoreLocation) -/// The velocity (measured in meters per second) at which the device is moving. -/// -/// This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms -/// that lack Core Location. On Apple platforms, you can use `CLLocationSpeed` anywhere you see this type. -public typealias LocationSpeed = CLLocationSpeed - -/// The accuracy of a geographical coordinate. -/// -/// This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms -/// that lack Core Location. On Apple platforms, you can use `CLLocationAccuracy` anywhere you see this type. -public typealias LocationAccuracy = CLLocationAccuracy -#else -/// The velocity (measured in meters per second) at which the device is moving. -public typealias LocationSpeed = Double - -/// The accuracy of a geographical coordinate. -public typealias LocationAccuracy = Double -#endif - -extension LocationCoordinate2D { - var requestDescription: String { - return "\(longitude.rounded(to: 1e6)),\(latitude.rounded(to: 1e6))" - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/Double.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/Double.swift deleted file mode 100644 index e6783052d..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Extensions/Double.swift +++ /dev/null @@ -1,5 +0,0 @@ -extension Double { - func rounded(to precision: Double) -> Double { - return (self * precision).rounded() / precision - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/ForeignMemberContainer.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/ForeignMemberContainer.swift deleted file mode 100644 index b47d18489..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Extensions/ForeignMemberContainer.swift +++ /dev/null @@ -1,117 +0,0 @@ -import Foundation -import Turf - -/// A coding key as an extensible enumeration. -struct AnyCodingKey: CodingKey { - var stringValue: String - var intValue: Int? - - init?(stringValue: String) { - self.stringValue = stringValue - self.intValue = nil - } - - init?(intValue: Int) { - self.stringValue = String(intValue) - self.intValue = intValue - } -} - -extension ForeignMemberContainer { - /// Decodes any foreign members using the given decoder. - mutating func decodeForeignMembers( - notKeyedBy _: WellKnownCodingKeys.Type, - with decoder: Decoder - ) throws where WellKnownCodingKeys: CodingKey { - guard (decoder.userInfo[.includesForeignMembers] as? Bool) == true else { return } - - let foreignMemberContainer = try decoder.container(keyedBy: AnyCodingKey.self) - for key in foreignMemberContainer.allKeys { - if WellKnownCodingKeys(stringValue: key.stringValue) == nil { - foreignMembers[key.stringValue] = try foreignMemberContainer.decode(JSONValue?.self, forKey: key) - } - } - } - - /// Encodes any foreign members using the given encoder. - func encodeForeignMembers(notKeyedBy _: WellKnownCodingKeys.Type, to encoder: Encoder) throws - where WellKnownCodingKeys: CodingKey { - guard (encoder.userInfo[.includesForeignMembers] as? Bool) == true else { return } - - var foreignMemberContainer = encoder.container(keyedBy: AnyCodingKey.self) - for (key, value) in foreignMembers { - if let key = AnyCodingKey(stringValue: key), - WellKnownCodingKeys(stringValue: key.stringValue) == nil - { - try foreignMemberContainer.encode(value, forKey: key) - } - } - } -} - -/// A class that can contain foreign members in arbitrary keys. -/// -/// When subclassing ``ForeignMemberContainerClass`` type, you should call -/// ``ForeignMemberContainerClass/decodeForeignMembers(notKeyedBy:with:)`` during your `Decodable.init(from:)` -/// initializer if your subclass has added any new properties. -/// -/// Structures should conform to the `ForeignMemberContainer` protocol instead of this protocol. -public protocol ForeignMemberContainerClass: AnyObject { - /// Foreign members to round-trip to JSON. - /// - /// Foreign members are unrecognized properties, similar to [foreign - /// members](https://datatracker.ietf.org/doc/html/rfc7946#section-6.1) in GeoJSON. This library does not officially - /// support any property that is documented as a “beta” property in the Mapbox Directions API response format, but - /// you can get and set it as an element of this `JSONObject`. - /// - /// Members are coded only if used `JSONEncoder` or `JSONDecoder` has `userInfo[.includesForeignMembers] = true`. - var foreignMembers: JSONObject { get set } - - /// Decodes any foreign members using the given decoder. - /// - Parameters: - /// - codingKeys: `CodingKeys` type which describes all properties declared in current subclass. - /// - decoder: `Decoder` instance, which performs the decoding process. - func decodeForeignMembers( - notKeyedBy codingKeys: WellKnownCodingKeys.Type, - with decoder: Decoder - ) throws where WellKnownCodingKeys: CodingKey & CaseIterable - - /// Encodes any foreign members using the given encoder. - /// - /// This method should be called in your `Encodable.encode(to:)` implementation only in the **base class**. - /// Otherwise it will not encode ``foreignMembers`` or way overwrite it. - /// - Parameter encoder: `Encoder` instance, performing the encoding process. - func encodeForeignMembers(to encoder: Encoder) throws -} - -extension ForeignMemberContainerClass { - public func decodeForeignMembers( - notKeyedBy _: WellKnownCodingKeys.Type, - with decoder: Decoder - ) throws where WellKnownCodingKeys: CodingKey & CaseIterable { - guard (decoder.userInfo[.includesForeignMembers] as? Bool) == true else { return } - - if foreignMembers.isEmpty { - let foreignMemberContainer = try decoder.container(keyedBy: AnyCodingKey.self) - for key in foreignMemberContainer.allKeys { - if WellKnownCodingKeys(stringValue: key.stringValue) == nil { - foreignMembers[key.stringValue] = try foreignMemberContainer.decode(JSONValue?.self, forKey: key) - } - } - } - WellKnownCodingKeys.allCases.forEach { - foreignMembers.removeValue(forKey: $0.stringValue) - } - } - - public func encodeForeignMembers(to encoder: Encoder) throws { - guard (encoder.userInfo[.includesForeignMembers] as? Bool) == true else { return } - - var foreignMemberContainer = encoder.container(keyedBy: AnyCodingKey.self) - for (key, value) in foreignMembers { - if let key = AnyCodingKey(stringValue: key) { - try foreignMemberContainer.encode(value, forKey: key) - } - } - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/GeoJSON.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/GeoJSON.swift deleted file mode 100644 index 3752d5482..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Extensions/GeoJSON.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import Turf - -extension BoundingBox: CustomStringConvertible { - public var description: String { - return "\(southWest.longitude),\(southWest.latitude);\(northEast.longitude),\(northEast.latitude)" - } -} - -extension LineString { - init(polyLineString: PolyLineString) throws { - switch polyLineString { - case .lineString(let lineString): - self = lineString - case .polyline(let encodedPolyline, precision: let precision): - self = try LineString(encodedPolyline: encodedPolyline, precision: precision) - } - } - - init(encodedPolyline: String, precision: Double) throws { - guard var coordinates = decodePolyline( - encodedPolyline, - precision: precision - ) as [LocationCoordinate2D]? else { - throw GeometryError.cannotDecodePolyline(precision: precision) - } - // If the polyline has zero length with both endpoints at the same coordinate, Polyline drops one of the - // coordinates. - // https://github.com/raphaelmor/Polyline/issues/59 - // Duplicate the coordinate to ensure a valid GeoJSON geometry. - if coordinates.count == 1 { - coordinates.append(coordinates[0]) - } -#if canImport(CoreLocation) - self.init(coordinates) -#else - self.init(coordinates.map { Turf.LocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) }) -#endif - } -} - -public enum GeometryError: LocalizedError { - case cannotDecodePolyline(precision: Double) - - public var failureReason: String? { - switch self { - case .cannotDecodePolyline(let precision): - return "Unable to decode the string as a polyline with precision \(precision)" - } - } - - public var recoverySuggestion: String? { - switch self { - case .cannotDecodePolyline: - return "Choose the precision that the string was encoded with." - } - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/HTTPURLResponse.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/HTTPURLResponse.swift deleted file mode 100644 index 0237c3060..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Extensions/HTTPURLResponse.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -extension HTTPURLResponse { - var rateLimit: UInt? { - guard let limit = allHeaderFields["X-Rate-Limit-Limit"] as? String else { - return nil - } - return UInt(limit) - } - - var rateLimitInterval: TimeInterval? { - guard let interval = allHeaderFields["X-Rate-Limit-Interval"] as? String else { - return nil - } - return TimeInterval(interval) - } - - var rateLimitResetTime: Date? { - guard let resetTime = allHeaderFields["X-Rate-Limit-Reset"] as? String else { - return nil - } - guard let resetTimeNumber = Double(resetTime) else { - return nil - } - return Date(timeIntervalSince1970: resetTimeNumber) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/Measurement.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/Measurement.swift deleted file mode 100644 index b761edcf1..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Extensions/Measurement.swift +++ /dev/null @@ -1,100 +0,0 @@ -import Foundation - -enum SpeedLimitDescriptor: Equatable { - enum UnitDescriptor: String, Codable { - case milesPerHour = "mph" - case kilometersPerHour = "km/h" - - init?(unit: UnitSpeed) { - switch unit { - case .milesPerHour: - self = .milesPerHour - case .kilometersPerHour: - self = .kilometersPerHour - default: - return nil - } - } - - var describedUnit: UnitSpeed { - switch self { - case .milesPerHour: - return .milesPerHour - case .kilometersPerHour: - return .kilometersPerHour - } - } - } - - enum CodingKeys: String, CodingKey { - case none - case speed - case unknown - case unit - } - - case none - case some(speed: Measurement) - case unknown - - init(speed: Measurement?) { - guard let speed else { - self = .unknown - return - } - - if speed.value.isInfinite { - self = .none - } else { - self = .some(speed: speed) - } - } -} - -extension SpeedLimitDescriptor: Codable { - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - if try (container.decodeIfPresent(Bool.self, forKey: .none)) ?? false { - self = .none - } else if try (container.decodeIfPresent(Bool.self, forKey: .unknown)) ?? false { - self = .unknown - } else { - let unitDescriptor = try container.decode(UnitDescriptor.self, forKey: .unit) - let unit = unitDescriptor.describedUnit - let value = try container.decode(Double.self, forKey: .speed) - self = .some(speed: .init(value: value, unit: unit)) - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - switch self { - case .none: - try container.encode(true, forKey: .none) - case .some(var speed): - let unitDescriptor = UnitDescriptor(unit: speed.unit) ?? { - speed = speed.converted(to: .kilometersPerHour) - return .kilometersPerHour - }() - try container.encode(unitDescriptor, forKey: .unit) - try container.encode(speed.value, forKey: .speed) - case .unknown: - try container.encode(true, forKey: .unknown) - } - } -} - -extension Measurement where UnitType == UnitSpeed { - init?(speedLimitDescriptor: SpeedLimitDescriptor) { - switch speedLimitDescriptor { - case .none: - self = .init(value: .infinity, unit: .kilometersPerHour) - case .some(let speed): - self = speed - case .unknown: - return nil - } - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/String.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/String.swift deleted file mode 100644 index abdaded63..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Extensions/String.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -extension String { - var nonEmptyString: String? { - return !isEmpty ? self : nil - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Extensions/URL+Request.swift b/ios/Classes/Navigation/MapboxDirections/Extensions/URL+Request.swift deleted file mode 100644 index b16165056..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Extensions/URL+Request.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -extension URL { - init(path: String, host: URL) { - guard let url = URL(string: path, relativeTo: host) else { - assertionFailure("Cannot form valid URL from '\(path)' relative to '\(host)'") - self = host - return - } - self = url - } -} - -extension URLRequest { - mutating func setupUserAgentString() { - setValue(userAgent, forHTTPHeaderField: "User-Agent") - } -} - -/// The user agent string for any HTTP requests performed directly within this library. -let userAgent: String = { - var components: [String] = [] - - if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? Bundle.main - .infoDictionary?["CFBundleIdentifier"] as? String - { - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" - components.append("\(appName)/\(version)") - } - - let libraryBundle: Bundle? = Bundle(for: Directions.self) - - if let libraryName = libraryBundle?.infoDictionary?["CFBundleName"] as? String, - let version = libraryBundle?.infoDictionary?["CFBundleShortVersionString"] as? String - { - components.append("\(libraryName)/\(version)") - } - - // `ProcessInfo().operatingSystemVersionString` can replace this when swift-corelibs-foundaton is next released: - // https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/ProcessInfo.swift#L104-L202 - let system: String -#if os(macOS) - system = "macOS" -#elseif os(iOS) - system = "iOS" -#elseif os(watchOS) - system = "watchOS" -#elseif os(tvOS) - system = "tvOS" -#elseif os(Linux) - system = "Linux" -#else - system = "unknown" -#endif - let systemVersion = ProcessInfo.processInfo.operatingSystemVersion - components - .append("\(system)/\(systemVersion.majorVersion).\(systemVersion.minorVersion).\(systemVersion.patchVersion)") - - let chip: String -#if arch(x86_64) - chip = "x86_64" -#elseif arch(arm) - chip = "arm" -#elseif arch(arm64) - chip = "arm64" -#elseif arch(i386) - chip = "i386" -#else - // Maybe fall back on `uname(2).machine`? - chip = "unrecognized" -#endif - - var simulator: String? -#if targetEnvironment(simulator) - simulator = "Simulator" -#endif - - let otherComponents = [ - chip, - simulator, - ].compactMap { $0 } - - components.append("(\(otherComponents.joined(separator: "; ")))") - - return components.joined(separator: " ") -}() diff --git a/ios/Classes/Navigation/MapboxDirections/Incident.swift b/ios/Classes/Navigation/MapboxDirections/Incident.swift deleted file mode 100644 index 37b859a49..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Incident.swift +++ /dev/null @@ -1,304 +0,0 @@ -import Foundation -import Turf - -/// `Incident` describes any corresponding event, used for annotating the route. -public struct Incident: Codable, Equatable, ForeignMemberContainer, Sendable { - public var foreignMembers: JSONObject = [:] - public var congestionForeignMembers: JSONObject = [:] - - private enum CodingKeys: String, CodingKey { - case identifier = "id" - case type - case description - case creationDate = "creation_time" - case startDate = "start_time" - case endDate = "end_time" - case impact - case subtype = "sub_type" - case subtypeDescription = "sub_type_description" - case alertCodes = "alertc_codes" - case lanesBlocked = "lanes_blocked" - case geometryIndexStart = "geometry_index_start" - case geometryIndexEnd = "geometry_index_end" - case countryCodeAlpha3 = "iso_3166_1_alpha3" - case countryCode = "iso_3166_1_alpha2" - case roadIsClosed = "closed" - case longDescription = "long_description" - case numberOfBlockedLanes = "num_lanes_blocked" - case congestionLevel = "congestion" - case affectedRoadNames = "affected_road_names" - } - - /// Defines known types of incidents. - /// - /// Each incident may or may not have specific set of data, depending on it's `kind` - public enum Kind: String, Sendable { - /// Accident - case accident - /// Congestion - case congestion - /// Construction - case construction - /// Disabled vehicle - case disabledVehicle = "disabled_vehicle" - /// Lane restriction - case laneRestriction = "lane_restriction" - /// Mass transit - case massTransit = "mass_transit" - /// Miscellaneous - case miscellaneous - /// Other news - case otherNews = "other_news" - /// Planned event - case plannedEvent = "planned_event" - /// Road closure - case roadClosure = "road_closure" - /// Road hazard - case roadHazard = "road_hazard" - /// Weather - case weather - - /// Undefined - case undefined - } - - /// Represents the impact of the incident on local traffic. - public enum Impact: String, Codable, Sendable { - /// Unknown impact - case unknown - /// Critical impact - case critical - /// Major impact - case major - /// Minor impact - case minor - /// Low impact - case low - } - - private struct CongestionContainer: Codable, ForeignMemberContainer, Sendable { - var foreignMembers: JSONObject = [:] - - // `Directions` define this as service value to indicate "no congestion calculated" - // see: https://docs.mapbox.com/api/navigation/directions/#incident-object - private static let CongestionUnavailableKey = 101 - - enum CodingKeys: String, CodingKey { - case value - } - - let value: Int - var clampedValue: Int? { - value == Self.CongestionUnavailableKey ? nil : value - } - - init(value: Int) { - self.value = value - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.value = try container.decode(Int.self, forKey: .value) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(value, forKey: .value) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - } - - /// Incident identifier - public var identifier: String - /// The kind of an incident - /// - /// This value is set to `nil` if ``kind`` value is not supported. - public var kind: Kind? { - return Kind(rawValue: rawKind) - } - - var rawKind: String - /// Short description of an incident. May be used as an additional info. - public var description: String - /// Date when incident item was created. - public var creationDate: Date - /// Date when incident happened. - public var startDate: Date - /// Date when incident shall end. - public var endDate: Date - /// Shows severity of an incident. May be not available for all incident types. - public var impact: Impact? - /// Provides additional classification of an incident. May be not available for all incident types. - public var subtype: String? - /// Breif description of the subtype. May be not available for all incident types and is not available if - /// ``subtype`` is `nil`. - public var subtypeDescription: String? - /// The three-letter ISO 3166-1 alpha-3 code for the country the incident is located in. Example: "USA". - public var countryCodeAlpha3: String? - /// The two-letter ISO 3166-1 alpha-2 code for the country the incident is located in. Example: "US". - public var countryCode: String? - /// If this is true then the road has been completely closed. - public var roadIsClosed: Bool? - /// A long description of the incident in a human-readable format. - public var longDescription: String? - /// The number of items in the ``lanesBlocked``. - public var numberOfBlockedLanes: Int? - /// Information about the amount of congestion on the road around the incident. - /// - /// A number between 0 and 100 representing the level of congestion caused by the incident. The higher the number, - /// the more congestion there is. A value of 0 means there is no congestion on the road. A value of 100 means that - /// the road is closed. - public var congestionLevel: NumericCongestionLevel? - /// List of roads names affected by the incident. - /// - /// Alternate road names are separated by a /. The list is ordered from the first affected road to the last one that - /// the incident lies on. - public var affectedRoadNames: [String]? - /// Contains list of ISO 14819-2:2013 codes - /// - /// See https://www.iso.org/standard/59231.html for details - public var alertCodes: Set - /// A list of lanes, affected by the incident - /// - /// `nil` value indicates that lanes data is not available - public var lanesBlocked: BlockedLanes? - /// The range of segments within the overall leg, where the incident spans. - public var shapeIndexRange: Range - - public init( - identifier: String, - type: Kind, - description: String, - creationDate: Date, - startDate: Date, - endDate: Date, - impact: Impact?, - subtype: String?, - subtypeDescription: String?, - alertCodes: Set, - lanesBlocked: BlockedLanes?, - shapeIndexRange: Range, - countryCodeAlpha3: String? = nil, - countryCode: String? = nil, - roadIsClosed: Bool? = nil, - longDescription: String? = nil, - numberOfBlockedLanes: Int? = nil, - congestionLevel: NumericCongestionLevel? = nil, - affectedRoadNames: [String]? = nil - ) { - self.identifier = identifier - self.rawKind = type.rawValue - self.description = description - self.creationDate = creationDate - self.startDate = startDate - self.endDate = endDate - self.impact = impact - self.subtype = subtype - self.subtypeDescription = subtypeDescription - self.alertCodes = alertCodes - self.lanesBlocked = lanesBlocked - self.shapeIndexRange = shapeIndexRange - self.countryCodeAlpha3 = countryCodeAlpha3 - self.countryCode = countryCode - self.roadIsClosed = roadIsClosed - self.longDescription = longDescription - self.numberOfBlockedLanes = numberOfBlockedLanes - self.congestionLevel = congestionLevel - self.affectedRoadNames = affectedRoadNames - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let formatter = ISO8601DateFormatter() - - self.identifier = try container.decode(String.self, forKey: .identifier) - self.rawKind = try container.decode(String.self, forKey: .type) - - self.description = try container.decode(String.self, forKey: .description) - - if let date = try formatter.date(from: container.decode(String.self, forKey: .creationDate)) { - self.creationDate = date - } else { - throw DecodingError.dataCorruptedError( - forKey: .creationDate, - in: container, - debugDescription: "`Intersection.creationTime` is encoded with invalid format." - ) - } - if let date = try formatter.date(from: container.decode(String.self, forKey: .startDate)) { - self.startDate = date - } else { - throw DecodingError.dataCorruptedError( - forKey: .startDate, - in: container, - debugDescription: "`Intersection.startTime` is encoded with invalid format." - ) - } - if let date = try formatter.date(from: container.decode(String.self, forKey: .endDate)) { - self.endDate = date - } else { - throw DecodingError.dataCorruptedError( - forKey: .endDate, - in: container, - debugDescription: "`Intersection.endTime` is encoded with invalid format." - ) - } - - self.impact = try container.decodeIfPresent(Impact.self, forKey: .impact) - self.subtype = try container.decodeIfPresent(String.self, forKey: .subtype) - self.subtypeDescription = try container.decodeIfPresent(String.self, forKey: .subtypeDescription) - self.alertCodes = try container.decode(Set.self, forKey: .alertCodes) - - self.lanesBlocked = try container.decodeIfPresent(BlockedLanes.self, forKey: .lanesBlocked) - - let geometryIndexStart = try container.decode(Int.self, forKey: .geometryIndexStart) - let geometryIndexEnd = try container.decode(Int.self, forKey: .geometryIndexEnd) - self.shapeIndexRange = geometryIndexStart.. 1 { - let context = EncodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Inconsistent valid indications." - ) - throw EncodingError.invalidValue(validIndications, context) - } - self.usableLaneIndication = validIndications.first - } else { - self.approachLanes = nil - self.usableApproachLanes = nil - self.preferredApproachLanes = nil - self.usableLaneIndication = nil - } - - self.outletRoadClasses = try container.decodeIfPresent(RoadClasses.self, forKey: .outletRoadClasses) - - let outletsArray = try container.decode([Bool].self, forKey: .outletIndexes) - self.outletIndexes = outletsArray.indices { $0 } - - self.outletIndex = try container.decodeIfPresent(Int.self, forKey: .outletIndex) - self.approachIndex = try container.decodeIfPresent(Int.self, forKey: .approachIndex) - - self.tollCollection = try container.decodeIfPresent(TollCollection.self, forKey: .tollCollection) - - self.tunnelName = try container.decodeIfPresent(String.self, forKey: .tunnelName) - - self.outletMapboxStreetsRoadClass = try container.decodeIfPresent( - MapboxStreetClassCodable.self, - forKey: .mapboxStreets - )?.streetClass - - self.isUrban = try container.decodeIfPresent(Bool.self, forKey: .isUrban) - - self.restStop = try container.decodeIfPresent(RestStop.self, forKey: .restStop) - - self.railroadCrossing = try container.decodeIfPresent(Bool.self, forKey: .railroadCrossing) - self.trafficSignal = try container.decodeIfPresent(Bool.self, forKey: .trafficSignal) - self.stopSign = try container.decodeIfPresent(Bool.self, forKey: .stopSign) - self.yieldSign = try container.decodeIfPresent(Bool.self, forKey: .yieldSign) - - self.interchange = try container.decodeIfPresent(Interchange.self, forKey: .interchange) - self.junction = try container.decodeIfPresent(Junction.self, forKey: .junction) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } -} - -extension Intersection { - public static func == (lhs: Intersection, rhs: Intersection) -> Bool { - return lhs.location == rhs.location && - lhs.headings == rhs.headings && - lhs.outletIndexes == rhs.outletIndexes && - lhs.approachIndex == rhs.approachIndex && - lhs.outletIndex == rhs.outletIndex && - lhs.approachLanes == rhs.approachLanes && - lhs.usableApproachLanes == rhs.usableApproachLanes && - lhs.preferredApproachLanes == rhs.preferredApproachLanes && - lhs.usableLaneIndication == rhs.usableLaneIndication && - lhs.restStop == rhs.restStop && - lhs.regionCode == rhs.regionCode && - lhs.outletMapboxStreetsRoadClass == rhs.outletMapboxStreetsRoadClass && - lhs.outletRoadClasses == rhs.outletRoadClasses && - lhs.tollCollection == rhs.tollCollection && - lhs.tunnelName == rhs.tunnelName && - lhs.isUrban == rhs.isUrban && - lhs.railroadCrossing == rhs.railroadCrossing && - lhs.trafficSignal == rhs.trafficSignal && - lhs.stopSign == rhs.stopSign && - lhs.yieldSign == rhs.yieldSign && - lhs.interchange == rhs.interchange && - lhs.junction == rhs.junction - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/IsochroneError.swift b/ios/Classes/Navigation/MapboxDirections/IsochroneError.swift deleted file mode 100644 index b7698e189..000000000 --- a/ios/Classes/Navigation/MapboxDirections/IsochroneError.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -/// An error that occurs when calculating isochrone contours. -public enum IsochroneError: LocalizedError { - public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { - if let response = response as? HTTPURLResponse { - switch (response.statusCode, code ?? "") { - case (200, "NoSegment"): - self = .unableToLocate - case (404, "ProfileNotFound"): - self = .profileNotFound - case (422, "InvalidInput"): - self = .invalidInput(message: message) - case (429, _): - self = .rateLimited( - rateLimitInterval: response.rateLimitInterval, - rateLimit: response.rateLimit, - resetTime: response.rateLimitResetTime - ) - default: - self = .unknown(response: response, underlying: error, code: code, message: message) - } - } else { - self = .unknown(response: response, underlying: error, code: code, message: message) - } - } - - /// There is no network connection available to perform the network request. - case network(_: URLError) - - /// The server returned a response that isn’t correctly formatted. - case invalidResponse(_: URLResponse?) - - /// The server returned an empty response. - case noData - - /// A specified location could not be associated with a roadway or pathway. - /// - /// Make sure the locations are close enough to a roadway or pathway. - case unableToLocate - - /// Unrecognized profile identifier. - /// - /// Make sure the ``IsochroneOptions/profileIdentifier`` option is set to one of the predefined values, such as - /// ``ProfileIdentifier/automobile``. - case profileNotFound - - /// The API recieved input that it didn't understand. - case invalidInput(message: String?) - - /// Too many requests have been made with the same access token within a certain period of time. - /// - /// Wait before retrying. - case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) - - /// Unknown error case. Look at associated values for more details. - case unknown(response: URLResponse?, underlying: Error?, code: String?, message: String?) -} diff --git a/ios/Classes/Navigation/MapboxDirections/IsochroneOptions.swift b/ios/Classes/Navigation/MapboxDirections/IsochroneOptions.swift deleted file mode 100644 index 29b828d43..000000000 --- a/ios/Classes/Navigation/MapboxDirections/IsochroneOptions.swift +++ /dev/null @@ -1,272 +0,0 @@ -import Foundation -import Turf - -#if canImport(UIKit) -import UIKit -#elseif canImport(AppKit) -import AppKit -#endif -#if canImport(CoreLocation) -import CoreLocation -#endif - -/// Options for calculating contours from the Mapbox Isochrone service. -public struct IsochroneOptions: Equatable, Sendable { - public init( - centerCoordinate: LocationCoordinate2D, - contours: Contours, - profileIdentifier: ProfileIdentifier = .automobile - ) { - self.centerCoordinate = centerCoordinate - self.contours = contours - self.profileIdentifier = profileIdentifier - } - - // MARK: Configuring the Contour - - /// Contours GeoJSON format. - public enum ContourFormat: Equatable, Sendable { - /// Requested contour will be presented as GeoJSON LineString. - case lineString - /// Requested contour will be presented as GeoJSON Polygon. - case polygon - } - - /// A string specifying the primary mode of transportation for the contours. - /// - /// The default value of this property is ``ProfileIdentifier/automobile``, which specifies driving directions. - public var profileIdentifier: ProfileIdentifier - /// A coordinate around which to center the isochrone lines. - public var centerCoordinate: LocationCoordinate2D - /// Contours bounds and color sheme definition. - public var contours: Contours - - /// Specifies the format of output contours. - /// - /// Defaults to ``ContourFormat/lineString`` which represents contours as linestrings. - public var contoursFormat: ContourFormat = .lineString - - /// Removes contours which are ``denoisingFactor`` times smaller than the biggest one. - /// - /// The default is 1.0. A value of 1.0 will only return the largest contour for a given value. A value of 0.5 drops - /// any contours that are less than half the area of the largest contour in the set of contours for that same value. - public var denoisingFactor: Float? - - /// Douglas-Peucker simplification tolerance. - /// - /// Higher means simpler geometries and faster performance. There is no upper bound. If no value is specified in the - /// request, the Isochrone API will choose the most optimized value to use for the request. - /// - /// - Note: Simplification of contours can lead to self-intersections, as well as intersections of adjacent - /// contours. - public var simplificationTolerance: LocationDistance? - - // MARK: Getting the Request URL - - /// The path of the request URL, specifying service name, version and profile. - var abridgedPath: String { - return "isochrone/v1/\(profileIdentifier.rawValue)" - } - - /// The path of the request URL, not including the hostname or any parameters. - var path: String { - return "\(abridgedPath)/\(centerCoordinate.requestDescription)" - } - - /// An array of URL query items (parameters) to include in an HTTP request. - public var urlQueryItems: [URLQueryItem] { - var queryItems: [URLQueryItem] = [] - - switch contours { - case .byDistances(let definitions): - let fallbackColor = definitions.allSatisfy { $0.color != nil } ? nil : Color.fallbackColor - - queryItems.append(URLQueryItem( - name: "contours_meters", - value: definitions.map { $0.queryValueDescription(roundingTo: .meters) } - .joined(separator: ",") - )) - - let colors = definitions.compactMap { $0.queryColorDescription(fallbackColor: fallbackColor) } - .joined(separator: ",") - if !colors.isEmpty { - queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) - } - case .byExpectedTravelTimes(let definitions): - let fallbackColor = definitions.allSatisfy { $0.color != nil } ? nil : Color.fallbackColor - - queryItems.append(URLQueryItem( - name: "contours_minutes", - value: definitions.map { $0.queryValueDescription(roundingTo: .minutes) } - .joined(separator: ",") - )) - - let colors = definitions.compactMap { $0.queryColorDescription(fallbackColor: fallbackColor) } - .joined(separator: ",") - if !colors.isEmpty { - queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) - } - } - - if contoursFormat == .polygon { - queryItems.append(URLQueryItem(name: "polygons", value: "true")) - } - - if let denoise = denoisingFactor { - queryItems.append(URLQueryItem(name: "denoise", value: String(denoise))) - } - - if let tolerance = simplificationTolerance { - queryItems.append(URLQueryItem(name: "generalize", value: String(tolerance))) - } - - return queryItems - } -} - -extension IsochroneOptions { - /// Definition of contours limits. - public enum Contours: Equatable, Sendable { - /// Describes Individual contour bound and color. - public struct Definition: Equatable, Sendable { - /// Bound measurement value. - public var value: Measurement - /// Contour fill color. - /// - /// If this property is unspecified, the contour is colored gray. If this property is not specified for any - /// contour, the contours are rainbow-colored. - public var color: Color? - - /// Initializes new contour Definition. - public init(value: Measurement, color: Color? = nil) { - self.value = value - self.color = color - } - - /// Initializes new contour Definition. - /// - /// Convenience initializer for encapsulating `Measurement` initialization. - public init(value: Double, unit: Unt, color: Color? = nil) { - self.init( - value: Measurement(value: value, unit: unit), - color: color - ) - } - - func queryValueDescription(roundingTo unit: Unt) -> String { - return String(Int(value.converted(to: unit).value.rounded())) - } - - func queryColorDescription(fallbackColor: Color?) -> String? { - return (color ?? fallbackColor)?.queryDescription - } - } - - /// The desired travel times to use for each isochrone contour. - /// - /// This value will be rounded to minutes. - case byExpectedTravelTimes([Definition]) - - /// The distances to use for each isochrone contour. - /// - /// Will be rounded to meters. - case byDistances([Definition]) - } -} - -extension IsochroneOptions { -#if canImport(UIKit) - /// RGB-based color representation for Isochrone contour. - public typealias Color = UIColor -#elseif canImport(AppKit) - /// RGB-based color representation for Isochrone contour. - public typealias Color = NSColor -#else - /// sRGB color space representation for Isochrone contour. - /// - /// This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple - /// platforms that lack `UIKit` or `AppKit`. On Apple platforms, you can use `UIColor` or `NSColor` respectively - /// anywhere you see this type. - public struct Color { - /// Red color component. - /// - /// Value ranged from `0` up to `255`. - public var red: Int - /// Green color component. - /// - /// Value ranged from `0` up to `255`. - public var green: Int - /// Blue color component. - /// - /// Value ranged from `0` up to `255`. - public var blue: Int - - /// Creates new `Color` instance. - public init(red: Int, green: Int, blue: Int) { - self.red = red - self.green = green - self.blue = blue - } - } -#endif -} - -extension IsochroneOptions.Color { - var queryDescription: String { - let hexFormat = "%02X%02X%02X" - -#if canImport(UIKit) - var red: CGFloat = 0 - var green: CGFloat = 0 - var blue: CGFloat = 0 - - getRed( - &red, - green: &green, - blue: &blue, - alpha: nil - ) - - return String( - format: hexFormat, - Int(red * 255), - Int(green * 255), - Int(blue * 255) - ) -#elseif canImport(AppKit) - var convertedColor = self - if colorSpace != .sRGB { - guard let converted = usingColorSpace(.sRGB) else { - assertionFailure("Failed to convert Isochrone contour color to RGB space.") - return "000000" - } - - convertedColor = converted - } - - return String( - format: hexFormat, - Int(convertedColor.redComponent * 255), - Int(convertedColor.greenComponent * 255), - Int(convertedColor.blueComponent * 255) - ) -#else - return String( - format: hexFormat, - red, - green, - blue - ) -#endif - } - - static var fallbackColor: IsochroneOptions.Color { -#if canImport(UIKit) - return gray -#elseif canImport(AppKit) - return gray -#else - return IsochroneOptions.Color(red: 128, green: 128, blue: 128) -#endif - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Isochrones.swift b/ios/Classes/Navigation/MapboxDirections/Isochrones.swift deleted file mode 100644 index 543dccbd7..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Isochrones.swift +++ /dev/null @@ -1,168 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif -#if canImport(CoreLocation) -import CoreLocation -#endif -import Turf - -/// Computes areas that are reachable within a specified amount of time or distance from a location, and returns the -/// reachable regions as contours of polygons or lines that you can display on a map. -open class Isochrones: @unchecked Sendable { - /// A tuple type representing the isochrone session that was generated from the request. - /// - Parameter options: A ``IsochroneOptions`` object representing the request parameter options. - /// - Parameter credentials: A object containing the credentials used to make the request. - public typealias Session = (options: IsochroneOptions, credentials: Credentials) - - /// A closure (block) to be called when a isochrone request is complete. - /// - /// - Parameter result: A `Result` enum that represents the `FeatureCollection` if the request returned - /// successfully, or the error if it did not. - public typealias IsochroneCompletionHandler = @MainActor @Sendable ( - _ result: Result - ) -> Void - - // MARK: Creating an Isochrones Object - - /// The Authorization & Authentication credentials that are used for this service. - /// - /// If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. - public let credentials: Credentials - private let urlSession: URLSession - private let processingQueue: DispatchQueue - - /// The shared isochrones object. - /// - /// To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be - /// specified in the `MBXAccessToken` key in the main application bundle’s Info.plist. - public static let shared: Isochrones = .init() - - /// Creates a new instance of Isochrones object. - /// - Parameters: - /// - credentials: Credentials that will be used to make API requests to Mapbox Isochrone API. - /// - urlSession: URLSession that will be used to submit API requests to Mapbox Isochrone API. - /// - processingQueue: A DispatchQueue that will be used for CPU intensive work. - public init( - credentials: Credentials = .init(), - urlSession: URLSession = .shared, - processingQueue: DispatchQueue = .global(qos: .userInitiated) - ) { - self.credentials = credentials - self.urlSession = urlSession - self.processingQueue = processingQueue - } - - /// Begins asynchronously calculating isochrone contours using the given options and delivers the results to a - /// closure. - /// This method retrieves the contours asynchronously from the [Mapbox Isochrone - /// API](https://docs.mapbox.com/api/navigation/isochrone/) over a network connection. If a connection error or - /// server error occurs, details about the error are passed into the given completion handler in lieu of the - /// contours. - /// - /// Contours may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). - /// - Parameters: - /// - options: An ``IsochroneOptions`` object specifying the requirements for the resulting contours. - /// - completionHandler: The closure (block) to call with the resulting contours. This closure is executed on the - /// application’s main thread. - /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to - /// execute, you no longer want the resulting contours, cancel this task. - @discardableResult - open func calculate( - _ options: IsochroneOptions, - completionHandler: @escaping IsochroneCompletionHandler - ) -> URLSessionDataTask { - let request = urlRequest(forCalculating: options) - let callCompletion = { @Sendable (_ result: Result) in - _ = Task { @MainActor in - completionHandler(result) - } - } - let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in - if let urlError = possibleError as? URLError { - callCompletion(.failure(.network(urlError))) - return - } - - guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - callCompletion(.failure(.invalidResponse(possibleResponse))) - return - } - - guard let data = possibleData else { - callCompletion(.failure(.noData)) - return - } - - self.processingQueue.async { - do { - let decoder = JSONDecoder() - - guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { - let apiError = IsochroneError( - code: nil, - message: nil, - response: possibleResponse, - underlyingError: possibleError - ) - - callCompletion(.failure(apiError)) - return - } - - guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { - let apiError = IsochroneError( - code: disposition.code, - message: disposition.message, - response: response, - underlyingError: possibleError - ) - callCompletion(.failure(apiError)) - return - } - - let result = try decoder.decode(FeatureCollection.self, from: data) - - callCompletion(.success(result)) - } catch { - let bailError = IsochroneError(code: nil, message: nil, response: response, underlyingError: error) - callCompletion(.failure(bailError)) - } - } - } - requestTask.priority = 1 - requestTask.resume() - - return requestTask - } - - // MARK: Request URL Preparation - - /// The GET HTTP URL used to fetch the contours from the API. - /// - /// - Parameter options: An ``IsochroneOptions`` object specifying the requirements for the resulting contours. - /// - Returns: The URL to send the request to. - open func url(forCalculating options: IsochroneOptions) -> URL { - var params = options.urlQueryItems - params.append(URLQueryItem(name: "access_token", value: credentials.accessToken)) - - let unparameterizedURL = URL(path: options.path, host: credentials.host) - var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! - components.queryItems = params - return components.url! - } - - /// The HTTP request used to fetch the contours from the API. - /// - /// - Parameter options: A ``IsochroneOptions`` object specifying the requirements for the resulting routes. - /// - Returns: A GET HTTP request to calculate the specified options. - open func urlRequest(forCalculating options: IsochroneOptions) -> URLRequest { - let getURL = url(forCalculating: options) - var request = URLRequest(url: getURL) - request.setupUserAgentString() - return request - } -} - -@available(*, unavailable) -extension Isochrones: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxDirections/Junction.swift b/ios/Classes/Navigation/MapboxDirections/Junction.swift deleted file mode 100644 index 590256382..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Junction.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -/// Contains information about routing and passing junction along the route. -public struct Junction: Codable, Equatable, Sendable { - /// The name of the junction, if available. - public let name: String? - - /// Initializes a new `Junction` object. - /// - Parameters: - /// - name: the name of the junction. - public init(name: String?) { - self.name = name - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Lane.swift b/ios/Classes/Navigation/MapboxDirections/Lane.swift deleted file mode 100644 index edabce1f4..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Lane.swift +++ /dev/null @@ -1,57 +0,0 @@ -import Foundation -import Turf - -/// A lane on the road approaching an intersection. -struct Lane: Equatable, ForeignMemberContainer { - var foreignMembers: JSONObject = [:] - - /// The lane indications specifying the maneuvers that may be executed from the lane. - let indications: LaneIndication - - /// Whether the lane can be taken to complete the maneuver (`true`) or not (`false`) - var isValid: Bool - - /// Whether the lane is a preferred lane (`true`) or not (`false`) - /// - /// A preferred lane is a lane that is recommended if there are multiple lanes available - var isActive: Bool? - - /// Which of the ``indications`` is applicable to the current route, when there is more than one - var validIndication: ManeuverDirection? - - init(indications: LaneIndication, valid: Bool = false, active: Bool? = false, preferred: ManeuverDirection? = nil) { - self.indications = indications - self.isValid = valid - self.isActive = active - self.validIndication = preferred - } -} - -extension Lane: Codable { - private enum CodingKeys: String, CodingKey { - case indications - case valid - case active - case preferred = "valid_indication" - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(indications, forKey: .indications) - try container.encode(isValid, forKey: .valid) - try container.encodeIfPresent(isActive, forKey: .active) - try container.encodeIfPresent(validIndication, forKey: .preferred) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.indications = try container.decode(LaneIndication.self, forKey: .indications) - self.isValid = try container.decode(Bool.self, forKey: .valid) - self.isActive = try container.decodeIfPresent(Bool.self, forKey: .active) - self.validIndication = try container.decodeIfPresent(ManeuverDirection.self, forKey: .preferred) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/LaneIndication.swift b/ios/Classes/Navigation/MapboxDirections/LaneIndication.swift deleted file mode 100644 index 527e7afb5..000000000 --- a/ios/Classes/Navigation/MapboxDirections/LaneIndication.swift +++ /dev/null @@ -1,134 +0,0 @@ -import Foundation - -/// Each of these options specifies a maneuver direction for which a given lane can be used. -/// -/// A Lane object has zero or more indications that usually correspond to arrows on signs or pavement markings. If no -/// options are specified, it may be the case that no maneuvers are indicated on signage or pavement markings for the -/// lane. -public struct LaneIndication: OptionSet, CustomStringConvertible, Sendable { - public var rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - /// Indicates a sharp turn to the right. - public static let sharpRight = LaneIndication(rawValue: 1 << 1) - - /// Indicates a turn to the right. - public static let right = LaneIndication(rawValue: 1 << 2) - - /// Indicates a turn to the right. - public static let slightRight = LaneIndication(rawValue: 1 << 3) - - /// Indicates no turn. - public static let straightAhead = LaneIndication(rawValue: 1 << 4) - - /// Indicates a slight turn to the left. - public static let slightLeft = LaneIndication(rawValue: 1 << 5) - - /// Indicates a turn to the left. - public static let left = LaneIndication(rawValue: 1 << 6) - - /// Indicates a sharp turn to the left. - public static let sharpLeft = LaneIndication(rawValue: 1 << 7) - - /// Indicates a U-turn. - public static let uTurn = LaneIndication(rawValue: 1 << 8) - - /// Creates a lane indication from the given description strings. - public init?(descriptions: [String]) { - var laneIndication: LaneIndication = [] - for description in descriptions { - switch description { - case "sharp right": - laneIndication.insert(.sharpRight) - case "right": - laneIndication.insert(.right) - case "slight right": - laneIndication.insert(.slightRight) - case "straight": - laneIndication.insert(.straightAhead) - case "slight left": - laneIndication.insert(.slightLeft) - case "left": - laneIndication.insert(.left) - case "sharp left": - laneIndication.insert(.sharpLeft) - case "uturn": - laneIndication.insert(.uTurn) - case "none": - break - default: - return nil - } - } - self.init(rawValue: laneIndication.rawValue) - } - - init?(from direction: ManeuverDirection) { - // Assuming that every possible raw value of ManeuverDirection matches valid raw value of LaneIndication - self.init(descriptions: [direction.rawValue]) - } - - public var descriptions: [String] { - if isEmpty { - return [] - } - - var descriptions: [String] = [] - if contains(.sharpRight) { - descriptions.append("sharp right") - } - if contains(.right) { - descriptions.append("right") - } - if contains(.slightRight) { - descriptions.append("slight right") - } - if contains(.straightAhead) { - descriptions.append("straight") - } - if contains(.slightLeft) { - descriptions.append("slight left") - } - if contains(.left) { - descriptions.append("left") - } - if contains(.sharpLeft) { - descriptions.append("sharp left") - } - if contains(.uTurn) { - descriptions.append("uturn") - } - return descriptions - } - - public var description: String { - return descriptions.joined(separator: ",") - } - - static func indications(from strings: [String], container: SingleValueDecodingContainer) throws -> LaneIndication { - guard let indications = self.init(descriptions: strings) else { - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Unable to initialize lane indications from decoded string. This should not happen." - ) - } - return indications - } -} - -extension LaneIndication: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let stringValues = try container.decode([String].self) - - self = try LaneIndication.indications(from: stringValues, container: container) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(descriptions) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/MapMatching/MapMatchingResponse.swift b/ios/Classes/Navigation/MapboxDirections/MapMatching/MapMatchingResponse.swift deleted file mode 100644 index f21236e46..000000000 --- a/ios/Classes/Navigation/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif -import Turf - -/// A ``MapMatchingResponse`` object is a structure that corresponds to a map matching response returned by the Mapbox -/// Map Matching API. -public struct MapMatchingResponse: ForeignMemberContainer { - public var foreignMembers: JSONObject = [:] - - /// The raw HTTP response from the Map Matching API. - public let httpResponse: HTTPURLResponse? - - /// An array of ``Match`` objects. - public var matches: [Match]? - - /// An array of ``Match/Tracepoint`` objects that represent the location an input point was matched with, in the - /// order in which they were matched. - /// This property will be `nil` if a trace point is omitted by the Map Matching API because it is an outlier. - public var tracepoints: [Match.Tracepoint?]? - - /// The criteria for the map matching response. - public let options: MatchOptions - - /// The credentials used to make the request. - public let credentials: Credentials - - /// The time when this ``MapMatchingResponse`` object was created, which is immediately upon recieving the raw URL - /// response. - /// - /// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to - /// `nil`; use the `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be - /// set to `nil` if you create this result from a JSON object or encoded object. - /// This property does not persist after encoding and decoding. - public var created: Date = .init() -} - -extension MapMatchingResponse: Codable { - private enum CodingKeys: String, CodingKey { - case matches = "matchings" - case tracepoints - } - - public init( - httpResponse: HTTPURLResponse?, - matches: [Match]? = nil, - tracepoints: [Match.Tracepoint]? = nil, - options: MatchOptions, - credentials: Credentials - ) { - self.httpResponse = httpResponse - self.matches = matches - self.tracepoints = tracepoints - self.options = options - self.credentials = credentials - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse - - guard let options = decoder.userInfo[.options] as? MatchOptions else { - throw DirectionsCodingError.missingOptions - } - self.options = options - - guard let credentials = decoder.userInfo[.credentials] as? Credentials else { - throw DirectionsCodingError.missingCredentials - } - self.credentials = credentials - - self.tracepoints = try container.decodeIfPresent([Match.Tracepoint?].self, forKey: .tracepoints) - self.matches = try container.decodeIfPresent([Match].self, forKey: .matches) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(matches, forKey: .matches) - try container.encodeIfPresent(tracepoints, forKey: .tracepoints) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/MapMatching/Match.swift b/ios/Classes/Navigation/MapboxDirections/MapMatching/Match.swift deleted file mode 100644 index ba6a69b35..000000000 --- a/ios/Classes/Navigation/MapboxDirections/MapMatching/Match.swift +++ /dev/null @@ -1,163 +0,0 @@ -import Foundation -import Turf - -/// A ``Weight`` enum represents the weight given to a specific ``Match`` by the Directions API. The default metric is a -/// compound index called "routability", which is duration-based with additional penalties for less desirable maneuvers. -public enum Weight: Equatable, Sendable { - case routability(value: Float) - case other(value: Float, metric: String) - - public init(value: Float, metric: String) { - switch metric { - case "routability": - self = .routability(value: value) - default: - self = .other(value: value, metric: metric) - } - } - - var metric: String { - switch self { - case .routability(value: _): - return "routability" - case .other(value: _, metric: let value): - return value - } - } - - var value: Float { - switch self { - case .routability(value: let weight): - return weight - case .other(value: let weight, metric: _): - return weight - } - } -} - -/// A ``Match`` object defines a single route that was created from a series of points that were matched against a road -/// network. -/// -/// Typically, you do not create instances of this class directly. Instead, you receive match objects when you pass a -/// ``MatchOptions`` object into the `Directions.calculate(_:completionHandler:)` method. -public struct Match: DirectionsResult { - public enum CodingKeys: String, CodingKey, CaseIterable { - case confidence - case weight - case weightName = "weight_name" - } - - public var shape: Turf.LineString? - - public var legs: [RouteLeg] - - public var distance: Turf.LocationDistance - - public var expectedTravelTime: TimeInterval - - public var typicalTravelTime: TimeInterval? - - public var speechLocale: Locale? - - public var fetchStartDate: Date? - - public var responseEndDate: Date? - - public var responseContainsSpeechLocale: Bool - - public var foreignMembers: Turf.JSONObject = [:] - - /// Initializes a match. - /// Typically, you do not create instances of this class directly. Instead, you receive match objects when you - /// request matches using the `Directions.calculate(_:completionHandler:)` method. - /// - /// - Parameters: - /// - legs: The legs that are traversed in order. - /// - shape: The matching roads or paths as a contiguous polyline. - /// - distance: The matched path’s cumulative distance, measured in meters. - /// - expectedTravelTime: The route’s expected travel time, measured in seconds. - /// - confidence: A number between 0 and 1 that indicates the Map Matching API’s confidence that the match is - /// accurate. A higher confidence means the match is more likely to be accurate. - /// - weight: A ``Weight`` enum, which represents the weight given to a specific ``Match``. - public init( - legs: [RouteLeg], - shape: LineString?, - distance: LocationDistance, - expectedTravelTime: TimeInterval, - confidence: Float, - weight: Weight - ) { - self.confidence = confidence - self.weight = weight - self.legs = legs - self.shape = shape - self.distance = distance - self.expectedTravelTime = expectedTravelTime - self.responseContainsSpeechLocale = false - } - - /// Creates a match from a decoder. - /// - /// - Precondition: If the decoder is decoding JSON data from an API response, the `Decoder.userInfo` dictionary - /// must contain a ``MatchOptions`` object in the ``Swift/CodingUserInfoKey/options`` key. If it does not, a - /// ``DirectionsCodingError/missingOptions`` error is thrown. - /// - /// - Parameter decoder: The decoder of JSON-formatted API response data or a previously encoded ``Match`` object. - public init(from decoder: Decoder) throws { - guard let options = decoder.userInfo[.options] as? DirectionsOptions else { - throw DirectionsCodingError.missingOptions - } - - let container = try decoder.container(keyedBy: DirectionsCodingKey.self) - self.legs = try Self.decodeLegs(using: container, options: options) - self.distance = try Self.decodeDistance(using: container) - self.expectedTravelTime = try Self.decodeExpectedTravelTime(using: container) - self.typicalTravelTime = try Self.decodeTypicalTravelTime(using: container) - self.shape = try Self.decodeShape(using: container) - self.speechLocale = try Self.decodeSpeechLocale(using: container) - self.responseContainsSpeechLocale = try Self.decodeResponseContainsSpeechLocale(using: container) - - self.confidence = try container.decode(Float.self, forKey: .match(.confidence)) - let weightValue = try container.decode(Float.self, forKey: .match(.weight)) - let weightMetric = try container.decode(String.self, forKey: .match(.weightName)) - - self.weight = Weight(value: weightValue, metric: weightMetric) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: DirectionsCodingKey.self) - try container.encode(confidence, forKey: .match(.confidence)) - try container.encode(weight.value, forKey: .match(.weight)) - try container.encode(weight.metric, forKey: .match(.weightName)) - - try encodeLegs(into: &container) - try encodeShape(into: &container, options: encoder.userInfo[.options] as? DirectionsOptions) - try encodeDistance(into: &container) - try encodeExpectedTravelTime(into: &container) - try encodeTypicalTravelTime(into: &container) - try encodeSpeechLocale(into: &container) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - /// A ``Weight`` enum, which represents the weight given to a specific ``Match``. - public var weight: Weight - - /// A number between 0 and 1 that indicates the Map Matching API’s confidence that the match is accurate. A higher - /// confidence means the match is more likely to be accurate. - public var confidence: Float -} - -extension Match: CustomStringConvertible { - public var description: String { - return legs.map(\.name).joined(separator: " – ") - } -} - -extension DirectionsCodingKey { - public static func match(_ key: Match.CodingKeys) -> Self { - .init(stringValue: key.stringValue) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/MapMatching/MatchOptions.swift b/ios/Classes/Navigation/MapboxDirections/MapMatching/MatchOptions.swift deleted file mode 100644 index 87df61750..000000000 --- a/ios/Classes/Navigation/MapboxDirections/MapMatching/MatchOptions.swift +++ /dev/null @@ -1,156 +0,0 @@ -import Foundation -#if canImport(CoreLocation) -import CoreLocation -#endif -import Turf - -/// A ``MatchOptions`` object is a structure that specifies the criteria for results returned by the Mapbox Map Matching -/// API. -/// -/// Pass an instance of this class into the `Directions.calculate(_:completionHandler:)` method. -open class MatchOptions: DirectionsOptions, @unchecked Sendable { - // MARK: Creating a Match Options Object - -#if canImport(CoreLocation) - /// Initializes a match options object for matching locations against the road network. - /// - Parameters: - /// - locations: An array of `CLLocation` objects representing locations to attempt to match against the road - /// network. The array should contain at least two locations (the source and destination) and at most 100 locations. - /// (Some profiles, such as ``ProfileIdentifier/automobileAvoidingTraffic``, [may have lower - /// limits](https://docs.mapbox.com/api/navigation/#directions).) - /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. - /// ``ProfileIdentifier/automobile`` is used by default. - /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. - public convenience init( - locations: [CLLocation], - profileIdentifier: ProfileIdentifier? = nil, - queryItems: [URLQueryItem]? = nil - ) { - let waypoints = locations.map { - Waypoint(location: $0) - } - self.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) - } -#endif - /// Initializes a match options object for matching geographic coordinates against the road network. - /// - Parameters: - /// - coordinates: An array of geographic coordinates representing locations to attempt to match against the road - /// network. The array should contain at least two locations (the source and destination) and at most 100 locations. - /// (Some profiles, such as ``ProfileIdentifier/automobileAvoidingTraffic``, [may have lower - /// limits](https://docs.mapbox.com/api/navigation/#directions).) Each coordinate is converted into a ``Waypoint`` - /// object. - /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. - /// ``ProfileIdentifier/automobile`` is used by default. - /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. - public convenience init( - coordinates: [LocationCoordinate2D], - profileIdentifier: ProfileIdentifier? = nil, - queryItems: [URLQueryItem]? = nil - ) { - let waypoints = coordinates.map { - Waypoint(coordinate: $0) - } - self.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) - } - - public required init( - waypoints: [Waypoint], - profileIdentifier: ProfileIdentifier? = nil, - queryItems: [URLQueryItem]? = nil - ) { - super.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) - - if queryItems?.contains(where: { queryItem in - queryItem.name == CodingKeys.resamplesTraces.stringValue && - queryItem.value == "true" - }) == true { - self.resamplesTraces = true - } - } - - private enum CodingKeys: String, CodingKey { - case resamplesTraces = "tidy" - } - - override public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(resamplesTraces, forKey: .resamplesTraces) - try super.encode(to: encoder) - } - - public required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.resamplesTraces = try container.decode(Bool.self, forKey: .resamplesTraces) - try super.init(from: decoder) - } - - // MARK: Resampling the Locations Before Matching - - /// If true, the input locations are re-sampled for improved map matching results. The default is `false`. - open var resamplesTraces: Bool = false - - // MARK: Separating the Matches Into Legs - - /// An index set containing indices of two or more items in ``DirectionsOptions/waypoints``. These will be - /// represented by - /// ``Waypoint``s in the resulting ``Match`` objects. - /// - /// Use this property when the ``DirectionsOptions/includesSteps`` property is `true` or when - /// ``DirectionsOptions/waypoints`` - /// represents a trace with a high sample rate. If this property is `nil`, the resulting ``Match`` objects contain a - /// waypoint for each coordinate in the match options. - /// - /// If specified, each index must correspond to a valid index in ``DirectionsOptions/waypoints``, and the index set - /// must contain 0 - /// and the last index (one less than `endIndex`) of ``DirectionsOptions/waypoints``. - @available(*, deprecated, message: "Use Waypoint.separatesLegs instead.") - open var waypointIndices: IndexSet? - - override var legSeparators: [Waypoint] { - if let indices = (self as MatchOptionsDeprecations).waypointIndices { - return indices.map { super.waypoints[$0] } - } else { - return super.legSeparators - } - } - - // MARK: Getting the Request URL - - override open var urlQueryItems: [URLQueryItem] { - var queryItems = super.urlQueryItems - - queryItems.append(URLQueryItem(name: "tidy", value: String(describing: resamplesTraces))) - - if let waypointIndices = (self as MatchOptionsDeprecations).waypointIndices { - queryItems.append(URLQueryItem(name: "waypoints", value: waypointIndices.map { - String(describing: $0) - }.joined(separator: ";"))) - } - - return queryItems - } - - override var abridgedPath: String { - return "matching/v5/\(profileIdentifier.rawValue)" - } -} - -private protocol MatchOptionsDeprecations { - var waypointIndices: IndexSet? { get set } -} - -extension MatchOptions: MatchOptionsDeprecations {} - -@available(*, unavailable) -extension MatchOptions: @unchecked Sendable {} - -// MARK: - Equatable - -extension MatchOptions { - public static func == (lhs: MatchOptions, rhs: MatchOptions) -> Bool { - let isSuperEqual = ((lhs as DirectionsOptions) == (rhs as DirectionsOptions)) - return isSuperEqual && - lhs.abridgedPath == rhs.abridgedPath && - lhs.resamplesTraces == rhs.resamplesTraces - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/MapMatching/Tracepoint.swift b/ios/Classes/Navigation/MapboxDirections/MapMatching/Tracepoint.swift deleted file mode 100644 index eb6af55a9..000000000 --- a/ios/Classes/Navigation/MapboxDirections/MapMatching/Tracepoint.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation -import Turf -#if canImport(CoreLocation) -import CoreLocation -#endif - -extension Match { - /// A tracepoint represents a location matched to the road network. - public struct Tracepoint: Codable, Equatable, Sendable { - private enum CodingKeys: String, CodingKey { - case coordinate = "location" - case countOfAlternatives = "alternatives_count" - case name - case matchingIndex = "matchings_index" - case waypointIndex = "waypoint_index" - } - - /// The geographic coordinate of the waypoint, snapped to the road network. - public var coordinate: LocationCoordinate2D - - /// Number of probable alternative matchings for this tracepoint. A value of zero indicates that this point was - /// matched unambiguously. - public var countOfAlternatives: Int - - /// The name of the road or path the coordinate snapped to. - public var name: String? - - /// The index of the match object in matchings that the sub-trace was matched to. - public var matchingIndex: Int - - /// The index of the waypoint inside the matched route. - /// - /// This value is set to`nil` for the silent waypoint when the corresponding waypoint has - /// ``Waypoint/separatesLegs`` set to `false`. - public var waypointIndex: Int? - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.coordinate = try container.decode( - LocationCoordinate2DCodable.self, - forKey: .coordinate - ).decodedCoordinates - self.countOfAlternatives = try container.decode(Int.self, forKey: .countOfAlternatives) - self.name = try container.decodeIfPresent(String.self, forKey: .name) - self.matchingIndex = try container.decode(Int.self, forKey: .matchingIndex) - self.waypointIndex = try container.decodeIfPresent(Int.self, forKey: .waypointIndex) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(LocationCoordinate2DCodable(coordinate), forKey: .coordinate) - try container.encode(countOfAlternatives, forKey: .countOfAlternatives) - try container.encode(name, forKey: .name) - try container.encode(matchingIndex, forKey: .matchingIndex) - try container.encode(waypointIndex, forKey: .waypointIndex) - } - - public init( - coordinate: LocationCoordinate2D, - countOfAlternatives: Int, - name: String? = nil, - matchingIndex: Int = 0, - waypointIndex: Int = 0 - ) { - self.coordinate = coordinate - self.countOfAlternatives = countOfAlternatives - self.name = name - self.matchingIndex = matchingIndex - self.waypointIndex = waypointIndex - } - } -} - -extension Match.Tracepoint: CustomStringConvertible { - public var description: String { - return "" - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/MapboxDirections.h b/ios/Classes/Navigation/MapboxDirections/MapboxDirections.h deleted file mode 100644 index 97e6721f7..000000000 --- a/ios/Classes/Navigation/MapboxDirections/MapboxDirections.h +++ /dev/null @@ -1,8 +0,0 @@ -#import -#import - -//! Project version number for MapboxDirections. -FOUNDATION_EXPORT double MapboxDirectionsVersionNumber; - -//! Project version string for MapboxDirections. -FOUNDATION_EXPORT const unsigned char MapboxDirectionsVersionString[]; diff --git a/ios/Classes/Navigation/MapboxDirections/MapboxStreetsRoadClass.swift b/ios/Classes/Navigation/MapboxDirections/MapboxStreetsRoadClass.swift deleted file mode 100644 index 40974dfab..000000000 --- a/ios/Classes/Navigation/MapboxDirections/MapboxStreetsRoadClass.swift +++ /dev/null @@ -1,56 +0,0 @@ - -import Foundation - -/// A road classification according to the [Mapbox Streets -/// source](https://docs.mapbox.com/vector-tiles/reference/mapbox-streets-v8/#road) , version 8. -public enum MapboxStreetsRoadClass: String, Codable, Equatable, Sendable { - /// High-speed, grade-separated highways - case motorway - /// Link roads/lanes/ramps connecting to motorways - case motorwayLink = "motorway_link" - /// Important roads that are not motorways. - case trunk - /// Link roads/lanes/ramps connecting to trunk roads - case trunkLink = "trunk_link" - /// A major highway linking large towns. - case primary - /// Link roads/lanes connecting to primary roads - case primaryLink = "primary_link" - /// A highway linking large towns. - case secondary - /// Link roads/lanes connecting to secondary roads - case secondaryLink = "secondary_link" - /// A road linking small settlements, or the local centres of a large town or city. - case tertiary - /// Link roads/lanes connecting to tertiary roads - case tertiaryLink = "tertiary_link" - /// Standard unclassified, residential, road, and living_street road types - case street - /// Streets that may have limited or no access for motor vehicles. - case streetLimited = "street_limited" - /// Includes pedestrian streets, plazas, and public transportation platforms. - case pedestrian - /// Includes motor roads under construction (but not service roads, paths, etc. - case construction - /// Roads mostly for agricultural and forestry use etc. - case track - /// Access roads, alleys, agricultural tracks, and other services roads. Also includes parking lot aisles, public & - /// private driveways. - case service - /// Those that serves automobiles and no or unspecified automobile service. - case ferry - /// Foot paths, cycle paths, ski trails. - case path - /// Railways, including mainline, commuter rail, and rapid transit. - case majorRail = "major_rail" - /// Includes light rail & tram lines. - case minorRail = "minor_rail" - /// Yard and service railways. - case serviceRail = "service_rail" - /// Ski lifts, gondolas, and other types of aerialway. - case aerialway - /// The approximate centerline of a golf course hole - case golf - /// Undefined - case undefined -} diff --git a/ios/Classes/Navigation/MapboxDirections/Matrix.swift b/ios/Classes/Navigation/MapboxDirections/Matrix.swift deleted file mode 100644 index e20b1e2f2..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Matrix.swift +++ /dev/null @@ -1,166 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -/// Computes distances and durations between origin-destination pairs, and returns the resulting distances in meters and -/// durations in seconds. -open class Matrix: @unchecked Sendable { - /// A tuple type representing the matrix session that was generated from the request. - /// - /// - Parameter options: A ``MatrixOptions`` object representing the request parameter options. - /// - Parameter credentials: A object containing the credentials used to make the request. - public typealias Session = (options: MatrixOptions, credentials: Credentials) - - /// A closure (block) to be called when a matrix request is complete. - /// - /// - parameter result: A `Result` enum that represents the (RETURN TYPE) if the request returned successfully, or - /// the error if it did not. - public typealias MatrixCompletionHandler = @Sendable ( - _ result: Result - ) -> Void - - // MARK: Creating an Matrix Object - - /// The Authorization & Authentication credentials that are used for this service. - /// - /// If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. - public let credentials: Credentials - private let urlSession: URLSession - private let processingQueue: DispatchQueue - - /// The shared matrix object. - /// - /// To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be - /// specified in the `MBXAccessToken` key in the main application bundle’s Info.plist. - public static let shared: Matrix = .init() - - /// Creates a new instance of Matrix object. - /// - Parameters: - /// - credentials: Credentials that will be used to make API requests to Mapbox Matrix API. - /// - urlSession: URLSession that will be used to submit API requests to Mapbox Matrix API. - /// - processingQueue: A DispatchQueue that will be used for CPU intensive work. - public init( - credentials: Credentials = .init(), - urlSession: URLSession = .shared, - processingQueue: DispatchQueue = .global(qos: .userInitiated) - ) { - self.credentials = credentials - self.urlSession = urlSession - self.processingQueue = processingQueue - } - - // MARK: Getting Matrix - - @discardableResult - /// Begins asynchronously calculating matrices using the given options and delivers the results to a closure. - /// - /// This method retrieves the matrices asynchronously from the [Mapbox Matrix - /// API](https://docs.mapbox.com/api/navigation/matrix/) over a network connection. If a connection error or server - /// error occurs, details about the error are passed into the given completion handler in lieu of the contours. - /// - Parameters: - /// - options: A ``MatrixOptions`` object specifying the requirements for the resulting matrices. - /// - completionHandler: The closure (block) to call with the resulting matrices. This closure is executed on the - /// application’s main thread. - /// - Returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to - /// execute, you no longer want the resulting matrices, cancel this task. - open func calculate( - _ options: MatrixOptions, - completionHandler: @escaping MatrixCompletionHandler - ) -> URLSessionDataTask { - let request = urlRequest(forCalculating: options) - let callCompletion = { @Sendable (_ result: Result) in - completionHandler(result) - } - let requestTask = urlSession.dataTask(with: request) { possibleData, possibleResponse, possibleError in - if let urlError = possibleError as? URLError { - callCompletion(.failure(.network(urlError))) - return - } - - guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { - callCompletion(.failure(.invalidResponse(possibleResponse))) - return - } - - guard let data = possibleData else { - callCompletion(.failure(.noData)) - return - } - - self.processingQueue.async { - do { - let decoder = JSONDecoder() - - guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { - let apiError = MatrixError( - code: nil, - message: nil, - response: response, - underlyingError: possibleError - ) - - callCompletion(.failure(apiError)) - return - } - - guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { - let apiError = MatrixError( - code: disposition.code, - message: disposition.message, - response: response, - underlyingError: possibleError - ) - - callCompletion(.failure(apiError)) - return - } - - let result = try decoder.decode(MatrixResponse.self, from: data) - - guard result.distances != nil || result.travelTimes != nil else { - callCompletion(.failure(.noRoute)) - return - } - - callCompletion(.success(result)) - - } catch { - let bailError = MatrixError(code: nil, message: nil, response: response, underlyingError: error) - callCompletion(.failure(bailError)) - } - } - } - requestTask.priority = 1 - requestTask.resume() - - return requestTask - } - - // MARK: Request URL Preparation - - /// The GET HTTP URL used to fetch the matrices from the Matrix API. - /// - /// - Parameter options: A ``MatrixOptions`` object specifying the requirements for the resulting contours. - /// - Returns: The URL to send the request to. - open func url(forCalculating options: MatrixOptions) -> URL { - var params = options.urlQueryItems - params.append(URLQueryItem(name: "access_token", value: credentials.accessToken)) - - let unparameterizedURL = URL(path: options.path, host: credentials.host) - var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! - components.queryItems = params - return components.url! - } - - /// The HTTP request used to fetch the matrices from the Matrix API. - /// - /// - Parameter options: A ``MatrixOptions`` object specifying the requirements for the resulting routes. - /// - Returns: A GET HTTP request to calculate the specified options. - open func urlRequest(forCalculating options: MatrixOptions) -> URLRequest { - let getURL = url(forCalculating: options) - var request = URLRequest(url: getURL) - request.setupUserAgentString() - return request - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/MatrixError.swift b/ios/Classes/Navigation/MapboxDirections/MatrixError.swift deleted file mode 100644 index 862c7934e..000000000 --- a/ios/Classes/Navigation/MapboxDirections/MatrixError.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -/// An error that occurs when computing matrices. -public enum MatrixError: LocalizedError { - public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { - if let response = response as? HTTPURLResponse { - switch (response.statusCode, code ?? "") { - case (200, "NoRoute"): - self = .noRoute - case (404, "ProfileNotFound"): - self = .profileNotFound - case (422, "InvalidInput"): - self = .invalidInput(message: message) - case (429, _): - self = .rateLimited( - rateLimitInterval: response.rateLimitInterval, - rateLimit: response.rateLimit, - resetTime: response.rateLimitResetTime - ) - default: - self = .unknown(response: response, underlying: error, code: code, message: message) - } - } else { - self = .unknown(response: response, underlying: error, code: code, message: message) - } - } - - /// There is no network connection available to perform the network request. - case network(_: URLError) - - /// The server returned a response that isn’t correctly formatted. - case invalidResponse(_: URLResponse?) - - /// The server returned an empty response. - case noData - - /// The API did not find a route for the given coordinates. Check for impossible routes or incorrectly formatted - /// coordinates. - case noRoute - - /// Unrecognized profile identifier. - /// - /// Make sure the ``MatrixOptions/profileIdentifier`` option is set to one of the predefined values, such as - /// ``ProfileIdentifier/automobile``. - case profileNotFound - - /// The API recieved input that it didn't understand. - /// - /// Make sure the number of approach elements matches the number of waypoints provided, and the number of waypoints - /// does not exceed the maximum number per request. - case invalidInput(message: String?) - - /// Too many requests have been made with the same access token within a certain period of time. - /// - /// Wait before retrying. - case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) - - /// Unknown error case. Look at associated values for more details. - case unknown(response: URLResponse?, underlying: Error?, code: String?, message: String?) -} diff --git a/ios/Classes/Navigation/MapboxDirections/MatrixOptions.swift b/ios/Classes/Navigation/MapboxDirections/MatrixOptions.swift deleted file mode 100644 index 070b3243f..000000000 --- a/ios/Classes/Navigation/MapboxDirections/MatrixOptions.swift +++ /dev/null @@ -1,204 +0,0 @@ -import Foundation -import Turf - -/// Options for calculating matrices from the Mapbox Matrix service. -public class MatrixOptions: Codable { - // MARK: Creating a Matrix Options Object - - /// Initializes a matrix options object for matrices and a given profile identifier. - /// - Parameters: - /// - sources: An array of ``Waypoint`` objects representing sources. - /// - destinations: An array of ``Waypoint`` objects representing destinations. - /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. - /// - /// - Note: `sources` and `destinations` should not be empty, otherwise matrix would not make sense. Total number of - /// waypoints may differ depending on the `profileIdentifier`. [See documentation for - /// details](https://docs.mapbox.com/api/navigation/matrix/#matrix-api-restrictions-and-limits). - public init(sources: [Waypoint], destinations: [Waypoint], profileIdentifier: ProfileIdentifier) { - self.profileIdentifier = profileIdentifier - self.waypointsData = .init( - sources: sources, - destinations: destinations - ) - } - - private let waypointsData: WaypointsData - - /// A string specifying the primary mode of transportation for the contours. - public var profileIdentifier: ProfileIdentifier - - /// An array of ``Waypoint`` objects representing locations that will be in the matrix. - public var waypoints: [Waypoint] { - return waypointsData.waypoints - } - - /// Attribute options for the matrix. - /// - /// Only ``AttributeOptions/distance`` and ``AttributeOptions/expectedTravelTime`` are supported. Empty - /// `attributeOptions` will result in default - /// values assumed. - public var attributeOptions: AttributeOptions = [] - - /// The ``Waypoint`` array that should be used as destinations. - /// - /// Must not be empty. - public var destinations: [Waypoint] { - get { - waypointsData.destinations - } - set { - waypointsData.destinations = newValue - } - } - - /// The ``Waypoint`` array that should be used as sources. - /// - /// Must not be empty. - public var sources: [Waypoint] { - get { - waypointsData.sources - } - set { - waypointsData.sources = newValue - } - } - - private enum CodingKeys: String, CodingKey { - case profileIdentifier = "profile" - case attributeOptions = "annotations" - case destinations - case sources - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(profileIdentifier, forKey: .profileIdentifier) - try container.encode(attributeOptions, forKey: .attributeOptions) - try container.encodeIfPresent(destinations, forKey: .destinations) - try container.encodeIfPresent(sources, forKey: .sources) - } - - public required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.profileIdentifier = try container.decode(ProfileIdentifier.self, forKey: .profileIdentifier) - self.attributeOptions = try container.decodeIfPresent(AttributeOptions.self, forKey: .attributeOptions) ?? [] - let destinations = try container.decodeIfPresent([Waypoint].self, forKey: .destinations) ?? [] - let sources = try container.decodeIfPresent([Waypoint].self, forKey: .sources) ?? [] - self.waypointsData = .init( - sources: sources, - destinations: destinations - ) - } - - // MARK: Getting the Request URL - - var coordinates: String? { - waypoints.map(\.coordinate.requestDescription).joined(separator: ";") - } - - /// An array of URL query items to include in an HTTP request. - var abridgedPath: String { - return "directions-matrix/v1/\(profileIdentifier.rawValue)" - } - - /// The path of the request URL, not including the hostname or any parameters. - var path: String { - guard let coordinates, - !coordinates.isEmpty - else { - assertionFailure("No query") - return "" - } - return "\(abridgedPath)/\(coordinates)" - } - - /// An array of URL query items (parameters) to include in an HTTP request. - public var urlQueryItems: [URLQueryItem] { - var queryItems: [URLQueryItem] = [] - - if !attributeOptions.isEmpty { - queryItems.append(URLQueryItem(name: "annotations", value: attributeOptions.description)) - } - - let mustArriveOnDrivingSide = !waypoints.filter { !$0.allowsArrivingOnOppositeSide }.isEmpty - if mustArriveOnDrivingSide { - let approaches = waypoints.map { $0.allowsArrivingOnOppositeSide ? "unrestricted" : "curb" } - queryItems.append(URLQueryItem(name: "approaches", value: approaches.joined(separator: ";"))) - } - - if waypoints.count != waypointsData.destinationsIndices.count { - let destinationString = waypointsData.destinationsIndices.map { String($0) }.joined(separator: ";") - queryItems.append(URLQueryItem(name: "destinations", value: destinationString)) - } - - if waypoints.count != waypointsData.sourcesIndices.count { - let sourceString = waypointsData.sourcesIndices.map { String($0) }.joined(separator: ";") - queryItems.append(URLQueryItem(name: "sources", value: sourceString)) - } - - return queryItems - } -} - -@available(*, unavailable) -extension MatrixOptions: @unchecked Sendable {} - -extension MatrixOptions: Equatable { - public static func == (lhs: MatrixOptions, rhs: MatrixOptions) -> Bool { - return lhs.profileIdentifier == rhs.profileIdentifier && - lhs.attributeOptions == rhs.attributeOptions && - lhs.sources == rhs.sources && - lhs.destinations == rhs.destinations - } -} - -extension MatrixOptions { - fileprivate class WaypointsData { - private(set) var waypoints: [Waypoint] = [] - var sources: [Waypoint] { - didSet { - updateWaypoints() - } - } - - var destinations: [Waypoint] { - didSet { - updateWaypoints() - } - } - - private(set) var sourcesIndices: IndexSet = [] - private(set) var destinationsIndices: IndexSet = [] - - private func updateWaypoints() { - sourcesIndices = [] - destinationsIndices = [] - - var destinations = destinations - for source in sources.enumerated() { - for destination in destinations.enumerated() { - if source.element == destination.element { - destinations.remove(at: destination.offset) - destinationsIndices.insert(source.offset) - break - } - } - } - - destinationsIndices.insert(integersIn: sources.endIndex..<(sources.endIndex + destinations.count)) - - var sum = sources - sum.append(contentsOf: destinations) - waypoints = sum - - sourcesIndices = IndexSet(integersIn: sources.indices) - } - - init(sources: [Waypoint], destinations: [Waypoint]) { - self.sources = sources - self.destinations = destinations - - updateWaypoints() - } - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/MatrixResponse.swift b/ios/Classes/Navigation/MapboxDirections/MatrixResponse.swift deleted file mode 100644 index 5fd2c1447..000000000 --- a/ios/Classes/Navigation/MapboxDirections/MatrixResponse.swift +++ /dev/null @@ -1,128 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif -import Turf - -public struct MatrixResponse: Sendable { - public typealias DistanceMatrix = [[LocationDistance?]] - public typealias DurationMatrix = [[TimeInterval?]] - - public let httpResponse: HTTPURLResponse? - - public let destinations: [Waypoint]? - public let sources: [Waypoint]? - - /// Array of arrays that represent the distances matrix in row-major order. - /// - /// `distances[i][j]` gives the route distance from the `i`'th `source` to the `j`'th `destination`. The distance - /// between the same coordinate is always `0`. Distance from `i` to `j` is not always the same as from `j` to `i`. - /// If a route cannot be found, the result is `nil`. - /// - /// - SeeAlso: ``distance(from:to:)`` - public let distances: DistanceMatrix? - - /// Array of arrays that represent the travel times matrix in row-major order. - /// - /// `travelTimes[i][j]` gives the travel time from the `i`'th `source` to the `j`'th `destination`. The duration - /// between the same coordinate is always `0`. Travel time from `i` to `j` is not always the same as from `j` to - /// `i`. If a duration cannot be found, the result is `nil`. - /// - /// - SeeAlso: ``travelTime(from:to:)`` - public let travelTimes: DurationMatrix? - - /// Returns route distance between specified source and destination. - /// - Parameters: - /// - sourceIndex: Index of a waypoint in the ``sources`` array. - /// - destinationIndex: Index of a waypoint in the ``destinations`` array. - /// - Returns: Calculated route distance between the points or `nil` if it is not available. - public func distance(from sourceIndex: Int, to destinationIndex: Int) -> LocationDistance? { - guard sources?.indices.contains(sourceIndex) ?? false, - destinations?.indices.contains(destinationIndex) ?? false - else { - return nil - } - return distances?[sourceIndex][destinationIndex] - } - - /// Returns expected travel time between specified source and destination. - /// - Parameters: - /// - sourceIndex: Index of a waypoint in the ``sources`` array. - /// - destinationIndex: Index of a waypoint in the ``destinations`` array. - /// - Returns: Calculated expected travel time between the points or `nil` if it is not available. - public func travelTime(from sourceIndex: Int, to destinationIndex: Int) -> TimeInterval? { - guard sources?.indices.contains(sourceIndex) ?? false, - destinations?.indices.contains(destinationIndex) ?? false - else { - return nil - } - return travelTimes?[sourceIndex][destinationIndex] - } -} - -extension MatrixResponse: Codable { - enum CodingKeys: String, CodingKey { - case distances - case durations - case destinations - case sources - } - - public init( - httpResponse: HTTPURLResponse?, - distances: DistanceMatrix?, - travelTimes: DurationMatrix?, - destinations: [Waypoint]?, - sources: [Waypoint]? - ) { - self.httpResponse = httpResponse - self.destinations = destinations - self.sources = sources - self.distances = distances - self.travelTimes = travelTimes - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - var distancesMatrix: DistanceMatrix = [] - var durationsMatrix: DurationMatrix = [] - - self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse - self.destinations = try container.decode([Waypoint].self, forKey: .destinations) - self.sources = try container.decode([Waypoint].self, forKey: .sources) - - if let decodedDistances = try container.decodeIfPresent([[Double?]].self, forKey: .distances) { - decodedDistances.forEach { distanceArray in - var distances: [LocationDistance?] = [] - distanceArray.forEach { distance in - distances.append(distance) - } - distancesMatrix.append(distances) - } - self.distances = distancesMatrix - } else { - self.distances = nil - } - - if let decodedDurations = try container.decodeIfPresent([[Double?]].self, forKey: .durations) { - decodedDurations.forEach { durationArray in - var durations: [TimeInterval?] = [] - durationArray.forEach { duration in - durations.append(duration) - } - durationsMatrix.append(durations) - } - self.travelTimes = durationsMatrix - } else { - self.travelTimes = nil - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(destinations, forKey: .destinations) - try container.encode(sources, forKey: .sources) - try container.encodeIfPresent(distances, forKey: .distances) - try container.encodeIfPresent(travelTimes, forKey: .durations) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/OfflineDirections.swift b/ios/Classes/Navigation/MapboxDirections/OfflineDirections.swift deleted file mode 100644 index 9379d703b..000000000 --- a/ios/Classes/Navigation/MapboxDirections/OfflineDirections.swift +++ /dev/null @@ -1,134 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif -import Turf - -public typealias OfflineVersion = String - -public typealias OfflineDownloaderCompletionHandler = @Sendable ( - _ location: URL?, - _ response: URLResponse?, - _ error: Error? -) -> Void - -public typealias OfflineDownloaderProgressHandler = @Sendable ( - _ bytesWritten: Int64, - _ totalBytesWritten: Int64, - _ totalBytesExpectedToWrite: Int64 -) -> Void - -public typealias OfflineVersionsHandler = @Sendable ( - _ version: [OfflineVersion]?, _ error: Error? -) -> Void - -struct AvailableVersionsResponse: Codable, Sendable { - let availableVersions: [String] -} - -public protocol OfflineDirectionsProtocol { - /// Fetches the available offline routing tile versions and returns them in descending chronological order. The most - /// recent version should typically be passed into ``downloadTiles(in:version:completionHandler:)``. - /// - /// - Parameter completionHandler: A closure of type ``OfflineVersionsHandler`` which will be called when the - /// request completes - func fetchAvailableOfflineVersions( - completionHandler: @escaping OfflineVersionsHandler - ) -> URLSessionDataTask - - /// Downloads offline routing tiles of the given version within the given coordinate bounds using the shared - /// URLSession. The tiles are written to disk at the location passed into the `completionHandler`. - /// - Parameters: - /// - coordinateBounds: The bounding box. - /// - version: The version to download. Version is represented as a String (yyyy-MM-dd-x). - /// - completionHandler: A closure of type ``OfflineDownloaderCompletionHandler`` which will be called when the - /// request completes. - /// - Returns: The Url session task. - func downloadTiles( - in coordinateBounds: BoundingBox, - version: OfflineVersion, - completionHandler: @escaping OfflineDownloaderCompletionHandler - ) -> URLSessionDownloadTask -} - -extension Directions: OfflineDirectionsProtocol { - /// The URL to a list of available versions. - public var availableVersionsURL: URL { - let url = credentials.host.appendingPathComponent("route-tiles/v1").appendingPathComponent("versions") - var components = URLComponents(url: url, resolvingAgainstBaseURL: true) - components?.queryItems = [URLQueryItem(name: "access_token", value: credentials.accessToken)] - return components!.url! - } - - /// Returns the URL to generate and download a tile pack from the Route Tiles API. - /// - Parameters: - /// - coordinateBounds: The coordinate bounds that the tiles should cover. - /// - version: A version obtained from ``availableVersionsURL``. - /// - Returns: The URL to generate and download the tile pack that covers the coordinate bounds. - public func tilesURL(for coordinateBounds: BoundingBox, version: OfflineVersion) -> URL { - let url = credentials.host.appendingPathComponent("route-tiles/v1") - .appendingPathComponent(coordinateBounds.description) - var components = URLComponents(url: url, resolvingAgainstBaseURL: true) - components?.queryItems = [ - URLQueryItem(name: "version", value: version), - URLQueryItem(name: "access_token", value: credentials.accessToken), - ] - return components!.url! - } - - /// Fetches the available offline routing tile versions and returns them in descending chronological order. The most - /// recent version should typically be passed into ``downloadTiles(in:version:completionHandler:)``. - /// - /// - Parameter completionHandler: A closure of type ``OfflineVersionsHandler`` which will be called when the - /// request completes. - @discardableResult - public func fetchAvailableOfflineVersions( - completionHandler: @escaping OfflineVersionsHandler - ) -> URLSessionDataTask { - let task = URLSession.shared.dataTask(with: availableVersionsURL) { data, _, error in - if let error { - completionHandler(nil, error) - return - } - - guard let data else { - completionHandler(nil, error) - return - } - - do { - let versionResponse = try JSONDecoder().decode(AvailableVersionsResponse.self, from: data) - let availableVersions = versionResponse.availableVersions.sorted(by: >) - completionHandler(availableVersions, error) - } catch { - completionHandler(nil, error) - } - } - - task.resume() - - return task - } - - /// Downloads offline routing tiles of the given version within the given coordinate bounds using the shared - /// URLSession. The tiles are written to disk at the location passed into the `completionHandler`. - /// - Parameters: - /// - coordinateBounds: The bounding box. - /// - version: The version to download. Version is represented as a String (yyyy-MM-dd-x). - /// - completionHandler: A closure of type ``OfflineDownloaderCompletionHandler`` which will be called when the - /// request completes. - /// - Returns: The Url session task. - @discardableResult - public func downloadTiles( - in coordinateBounds: BoundingBox, - version: OfflineVersion, - completionHandler: @escaping OfflineDownloaderCompletionHandler - ) -> URLSessionDownloadTask { - let url = tilesURL(for: coordinateBounds, version: version) - let task: URLSessionDownloadTask = URLSession.shared.downloadTask(with: url) { - completionHandler($0, $1, $2) - } - task.resume() - return task - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Polyline.swift b/ios/Classes/Navigation/MapboxDirections/Polyline.swift deleted file mode 100644 index bd4afe3f0..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Polyline.swift +++ /dev/null @@ -1,397 +0,0 @@ -// Polyline.swift -// -// Copyright (c) 2015 Raphaël Mor -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import Foundation -#if canImport(CoreLocation) -import CoreLocation -#endif - -public typealias LocationCoordinate2D = CLLocationCoordinate2D - -// MARK: - Public Classes - - -/// This class can be used for : -/// -/// - Encoding an [CLLocation] or a [CLLocationCoordinate2D] to a polyline String -/// - Decoding a polyline String to an [CLLocation] or a [CLLocationCoordinate2D] -/// - Encoding / Decoding associated levels -/// -/// it is aims to produce the same results as google's iOS sdk not as the online -/// tool which is fuzzy when it comes to rounding values -/// -/// it is based on google's algorithm that can be found here : -/// -/// :see: https://developers.google.com/maps/documentation/utilities/polylinealgorithm -public struct Polyline { - /// The array of coordinates (nil if polyline cannot be decoded) - public let coordinates: [LocationCoordinate2D]? - /// The encoded polyline - public let encodedPolyline: String - - /// The array of levels (nil if cannot be decoded, or is not provided) - public let levels: [UInt32]? - /// The encoded levels (nil if cannot be encoded, or is not provided) - public let encodedLevels: String? - -/// The array of location (computed from coordinates) -#if canImport(CoreLocation) - public var locations: [CLLocation]? { - return coordinates.map(toLocations) - } -#endif - - // MARK: - Public Methods - - - /// This designated initializer encodes a `[CLLocationCoordinate2D]` - /// - /// - parameter coordinates: The `Array` of `LocationCoordinate2D`s (that is, `CLLocationCoordinate2D`s) that you - /// want to encode - /// - parameter levels: The optional `Array` of levels that you want to encode (default: `nil`) - /// - parameter precision: The precision used for encoding (default: `1e5`) - public init(coordinates: [LocationCoordinate2D], levels: [UInt32]? = nil, precision: Double = 1e5) { - self.coordinates = coordinates - self.levels = levels - - self.encodedPolyline = encodeCoordinates(coordinates, precision: precision) - - self.encodedLevels = levels.map(encodeLevels) - } - - /// This designated initializer decodes a polyline `String` - /// - /// - parameter encodedPolyline: The polyline that you want to decode - /// - parameter encodedLevels: The levels that you want to decode (default: `nil`) - /// - parameter precision: The precision used for decoding (default: `1e5`) - public init(encodedPolyline: String, encodedLevels: String? = nil, precision: Double = 1e5) { - self.encodedPolyline = encodedPolyline - self.encodedLevels = encodedLevels - - self.coordinates = decodePolyline(encodedPolyline, precision: precision) - - self.levels = self.encodedLevels.flatMap(decodeLevels) - } - -#if canImport(CoreLocation) - /// This init encodes a `[CLLocation]` - /// - /// - parameter locations: The `Array` of `CLLocation` that you want to encode - /// - parameter levels: The optional array of levels that you want to encode (default: `nil`) - /// - parameter precision: The precision used for encoding (default: `1e5`) - public init(locations: [CLLocation], levels: [UInt32]? = nil, precision: Double = 1e5) { - let coordinates = toCoordinates(locations) - self.init(coordinates: coordinates, levels: levels, precision: precision) - } -#endif -} - -// MARK: - Public Functions - - -/// This function encodes an `[CLLocationCoordinate2D]` to a `String` -/// -/// - parameter coordinates: The `Array` of `LocationCoordinate2D`s (that is, `CLLocationCoordinate2D`s) that you want -/// to encode -/// - parameter precision: The precision used to encode coordinates (default: `1e5`) -/// -/// - returns: A `String` representing the encoded Polyline -public func encodeCoordinates(_ coordinates: [LocationCoordinate2D], precision: Double = 1e5) -> String { - var previousCoordinate = IntegerCoordinates(0, 0) - var encodedPolyline = "" - - for coordinate in coordinates { - let intLatitude = Int(round(coordinate.latitude * precision)) - let intLongitude = Int(round(coordinate.longitude * precision)) - - let coordinatesDifference = ( - intLatitude - previousCoordinate.latitude, - intLongitude - previousCoordinate.longitude - ) - - encodedPolyline += encodeCoordinate(coordinatesDifference) - - previousCoordinate = (intLatitude, intLongitude) - } - - return encodedPolyline -} - -#if canImport(CoreLocation) -/// This function encodes an `[CLLocation]` to a `String` -/// -/// - parameter coordinates: The `Array` of `CLLocation` that you want to encode -/// - parameter precision: The precision used to encode locations (default: `1e5`) -/// -/// - returns: A `String` representing the encoded Polyline -public func encodeLocations(_ locations: [CLLocation], precision: Double = 1e5) -> String { - return encodeCoordinates(toCoordinates(locations), precision: precision) -} -#endif - -/// This function encodes an `[UInt32]` to a `String` -/// -/// - parameter levels: The `Array` of `UInt32` levels that you want to encode -/// -/// - returns: A `String` representing the encoded Levels -public func encodeLevels(_ levels: [UInt32]) -> String { - return levels.reduce("") { - $0 + encodeLevel($1) - } -} - -/// This function decodes a `String` to a `[CLLocationCoordinate2D]?` -/// -/// - parameter encodedPolyline: `String` representing the encoded Polyline -/// - parameter precision: The precision used to decode coordinates (default: `1e5`) -/// -/// - returns: A `[CLLocationCoordinate2D]` representing the decoded polyline if valid, `nil` otherwise -public func decodePolyline(_ encodedPolyline: String, precision: Double = 1e5) -> [LocationCoordinate2D]? { - let data = encodedPolyline.data(using: .utf8)! - return data.withUnsafeBytes { byteArray -> [LocationCoordinate2D]? in - let length = data.count - var position = 0 - - var decodedCoordinates = [LocationCoordinate2D]() - - var lat = 0.0 - var lon = 0.0 - - while position < length { - do { - let resultingLat = try decodeSingleCoordinate( - byteArray: byteArray, - length: length, - position: &position, - precision: precision - ) - lat += resultingLat - - let resultingLon = try decodeSingleCoordinate( - byteArray: byteArray, - length: length, - position: &position, - precision: precision - ) - lon += resultingLon - } catch { - return nil - } - - decodedCoordinates.append(LocationCoordinate2D(latitude: lat, longitude: lon)) - } - - return decodedCoordinates - } -} - -#if canImport(CoreLocation) -/// This function decodes a String to a [CLLocation]? -/// -/// - parameter encodedPolyline: String representing the encoded Polyline -/// - parameter precision: The precision used to decode locations (default: 1e5) -/// -/// - returns: A [CLLocation] representing the decoded polyline if valid, nil otherwise -public func decodePolyline(_ encodedPolyline: String, precision: Double = 1e5) -> [CLLocation]? { - return decodePolyline(encodedPolyline, precision: precision).map(toLocations) -} -#endif - -/// This function decodes a `String` to an `[UInt32]` -/// -/// - parameter encodedLevels: The `String` representing the levels to decode -/// -/// - returns: A `[UInt32]` representing the decoded Levels if the `String` is valid, `nil` otherwise -public func decodeLevels(_ encodedLevels: String) -> [UInt32]? { - var remainingLevels = encodedLevels.unicodeScalars - var decodedLevels = [UInt32]() - - while remainingLevels.count > 0 { - do { - let chunk = try extractNextChunk(&remainingLevels) - let level = decodeLevel(chunk) - decodedLevels.append(level) - } catch { - return nil - } - } - - return decodedLevels -} - -// MARK: - Private - - -// MARK: Encode Coordinate - -private func encodeCoordinate(_ locationCoordinate: IntegerCoordinates) -> String { - let latitudeString = encodeSingleComponent(locationCoordinate.latitude) - let longitudeString = encodeSingleComponent(locationCoordinate.longitude) - - return latitudeString + longitudeString -} - -private func encodeSingleComponent(_ value: Int) -> String { - var intValue = value - - if intValue < 0 { - intValue = intValue << 1 - intValue = ~intValue - } else { - intValue = intValue << 1 - } - - return encodeFiveBitComponents(intValue) -} - -// MARK: Encode Levels - -private func encodeLevel(_ level: UInt32) -> String { - return encodeFiveBitComponents(Int(level)) -} - -private func encodeFiveBitComponents(_ value: Int) -> String { - var remainingComponents = value - - var fiveBitComponent = 0 - var returnString = String() - - repeat { - fiveBitComponent = remainingComponents & 0x1F - - if remainingComponents >= 0x20 { - fiveBitComponent |= 0x20 - } - - fiveBitComponent += 63 - - let char = UnicodeScalar(fiveBitComponent)! - returnString.append(String(char)) - remainingComponents = remainingComponents >> 5 - } while remainingComponents != 0 - - return returnString -} - -// MARK: Decode Coordinate - -// We use a byte array (UnsafePointer) here for performance reasons. Check with swift 2 if we can -// go back to using [Int8] -private func decodeSingleCoordinate( - byteArray: UnsafeRawBufferPointer, - length: Int, - position: inout Int, - precision: Double = 1e5 -) throws -> Double { - guard position < length else { throw PolylineError.singleCoordinateDecodingError } - - let bitMask = Int8(0x1F) - - var coordinate: Int32 = 0 - - var currentChar: Int8 - var componentCounter: Int32 = 0 - var component: Int32 = 0 - - repeat { - currentChar = Int8(byteArray[position]) - 63 - component = Int32(currentChar & bitMask) - coordinate |= (component << (5 * componentCounter)) - position += 1 - componentCounter += 1 - } while ((currentChar & 0x20) == 0x20) && (position < length) && (componentCounter < 6) - - if componentCounter == 6, (currentChar & 0x20) == 0x20 { - throw PolylineError.singleCoordinateDecodingError - } - - if (coordinate & 0x01) == 0x01 { - coordinate = ~(coordinate >> 1) - } else { - coordinate = coordinate >> 1 - } - - return Double(coordinate) / precision -} - -// MARK: Decode Levels - -private func extractNextChunk(_ encodedString: inout String.UnicodeScalarView) throws -> String { - var currentIndex = encodedString.startIndex - - while currentIndex != encodedString.endIndex { - let currentCharacterValue = Int32(encodedString[currentIndex].value) - if isSeparator(currentCharacterValue) { - let extractedScalars = encodedString[encodedString.startIndex...currentIndex] - encodedString = String - .UnicodeScalarView(encodedString[encodedString.index(after: currentIndex).. UInt32 { - let scalarArray = [] + encodedLevel.unicodeScalars - - return UInt32(agregateScalarArray(scalarArray)) -} - -private func agregateScalarArray(_ scalars: [UnicodeScalar]) -> Int32 { - let lastValue = Int32(scalars.last!.value) - - let fiveBitComponents: [Int32] = scalars.map { scalar in - let value = Int32(scalar.value) - if value != lastValue { - return (value - 63) ^ 0x20 - } else { - return value - 63 - } - } - - return Array(fiveBitComponents.reversed()).reduce(0) { ($0 << 5) | $1 } -} - -// MARK: Utilities - -enum PolylineError: Error { - case singleCoordinateDecodingError - case chunkExtractingError -} - -#if canImport(CoreLocation) -private func toCoordinates(_ locations: [CLLocation]) -> [CLLocationCoordinate2D] { - return locations.map { location in location.coordinate } -} - -private func toLocations(_ coordinates: [CLLocationCoordinate2D]) -> [CLLocation] { - return coordinates.map { coordinate in - CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) - } -} -#endif - -private func isSeparator(_ value: Int32) -> Bool { - return (value - 63) & 0x20 != 0x20 -} - -private typealias IntegerCoordinates = (latitude: Int, longitude: Int) diff --git a/ios/Classes/Navigation/MapboxDirections/ProfileIdentifier.swift b/ios/Classes/Navigation/MapboxDirections/ProfileIdentifier.swift deleted file mode 100644 index 82fe513c2..000000000 --- a/ios/Classes/Navigation/MapboxDirections/ProfileIdentifier.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -/// Options determining the primary mode of transportation. -public struct ProfileIdentifier: Codable, Hashable, RawRepresentable, Sendable { - public init(rawValue: String) { - self.rawValue = rawValue - } - - public var rawValue: String - - /// The returned directions are appropriate for driving or riding a car, truck, or motorcycle. - /// - /// This profile prioritizes fast routes by preferring high-speed roads like highways. A driving route may use a - /// ferry where necessary. - public static let automobile: ProfileIdentifier = .init(rawValue: "mapbox/driving") - - /// The returned directions are appropriate for driving or riding a car, truck, or motorcycle. - /// - /// This profile avoids traffic congestion based on current traffic data. A driving route may use a ferry where - /// necessary. - /// - /// Traffic data is available in [a number of countries and territories - /// worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/#traffic-data). Where traffic data is - /// unavailable, this profile prefers high-speed roads like highways, similar to ``ProfileIdentifier/automobile``. - /// - /// - Note: This profile is not supported by ``Isochrones`` API. - public static let automobileAvoidingTraffic: ProfileIdentifier = .init(rawValue: "mapbox/driving-traffic") - - /// The returned directions are appropriate for riding a bicycle. - /// - /// This profile prioritizes short, safe routes by avoiding highways and preferring cycling infrastructure, such as - /// bike lanes on surface streets. A cycling route may, where necessary, use other modes of transportation, such as - /// ferries or trains, or require dismounting the bicycle for a distance. - public static let cycling: ProfileIdentifier = .init(rawValue: "mapbox/cycling") - - /// The returned directions are appropriate for walking or hiking. - /// - /// This profile prioritizes short routes, making use of sidewalks and trails where available. A walking route may - /// use other modes of transportation, such as ferries or trains, where necessary. - public static let walking: ProfileIdentifier = .init(rawValue: "mapbox/walking") -} - -@available(*, deprecated, renamed: "ProfileIdentifier") -public typealias MBDirectionsProfileIdentifier = ProfileIdentifier - -/// Options determining the primary mode of transportation for the routes. -@available(*, deprecated, renamed: "ProfileIdentifier") -public typealias DirectionsProfileIdentifier = ProfileIdentifier diff --git a/ios/Classes/Navigation/MapboxDirections/QuickLook.swift b/ios/Classes/Navigation/MapboxDirections/QuickLook.swift deleted file mode 100644 index 5aa6863c3..000000000 --- a/ios/Classes/Navigation/MapboxDirections/QuickLook.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation -import Turf - -/// A type with a customized Quick Look representation in the Xcode debugger. -protocol CustomQuickLookConvertible { - /// Returns a [Quick Look–compatible](https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/CustomClassDisplay_in_QuickLook/CH02-std_objects_support/CH02-std_objects_support.html#//apple_ref/doc/uid/TP40014001-CH3-SW19) - /// representation for display in the Xcode debugger. - func debugQuickLookObject() -> Any? -} - -/// Returns a URL to an image representation of the given coordinates via the [Mapbox Static Images -/// API](https://docs.mapbox.com/api/maps/#static-images). -func debugQuickLookURL( - illustrating shape: LineString, - profileIdentifier: ProfileIdentifier = .automobile, - accessToken: String? = defaultAccessToken -) -> URL? { - guard let accessToken else { - return nil - } - - let styleIdentifier: String - let identifierOfLayerAboveOverlays: String - switch profileIdentifier { - case .automobileAvoidingTraffic: - styleIdentifier = "mapbox/navigation-preview-day-v4" - identifierOfLayerAboveOverlays = "waterway-label" - case .cycling, .walking: - styleIdentifier = "mapbox/outdoors-v11" - identifierOfLayerAboveOverlays = "contour-label" - default: - styleIdentifier = "mapbox/streets-v11" - identifierOfLayerAboveOverlays = "building-number-label" - } - let styleIdentifierComponent = "/\(styleIdentifier)/static" - - var allowedCharacterSet = CharacterSet.urlPathAllowed - allowedCharacterSet.remove(charactersIn: "/)") - let encodedPolyline = shape.polylineEncodedString(precision: 1e5) - .addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! - let overlaysComponent = "/path-10+3802DA-0.6(\(encodedPolyline))" - - let path = "/styles/v1\(styleIdentifierComponent)\(overlaysComponent)/auto/680x360@2x" - - var components = URLComponents() - components.queryItems = [ - URLQueryItem(name: "before_layer", value: identifierOfLayerAboveOverlays), - URLQueryItem(name: "access_token", value: accessToken), - ] - - return URL( - string: "\(defaultApiEndPointURLString ?? "https://api.mapbox.com")\(path)?\(components.percentEncodedQuery!)" - ) -} diff --git a/ios/Classes/Navigation/MapboxDirections/RefreshedRoute.swift b/ios/Classes/Navigation/MapboxDirections/RefreshedRoute.swift deleted file mode 100644 index be4632bf7..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RefreshedRoute.swift +++ /dev/null @@ -1,65 +0,0 @@ -import Foundation -import Turf - -/// A skeletal route containing only the information about the route that has been refreshed. -public struct RefreshedRoute: ForeignMemberContainer, Equatable { - public var foreignMembers: JSONObject = [:] - - /// The legs along the route, starting at the first refreshed leg index. - public var legs: [RefreshedRouteLeg] -} - -extension RefreshedRoute: Codable { - enum CodingKeys: String, CodingKey { - case legs - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.legs = try container.decode([RefreshedRouteLeg].self, forKey: .legs) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(legs, forKey: .legs) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } -} - -/// A skeletal route leg containing only the information about the route leg that has been refreshed. -public struct RefreshedRouteLeg: ForeignMemberContainer, Equatable { - public var foreignMembers: JSONObject = [:] - - public var attributes: RouteLeg.Attributes - public var incidents: [Incident]? - public var closures: [RouteLeg.Closure]? -} - -extension RefreshedRouteLeg: Codable { - enum CodingKeys: String, CodingKey { - case attributes = "annotation" - case incidents - case closures - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.attributes = try container.decode(RouteLeg.Attributes.self, forKey: .attributes) - self.incidents = try container.decodeIfPresent([Incident].self, forKey: .incidents) - self.closures = try container.decodeIfPresent([RouteLeg.Closure].self, forKey: .closures) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(attributes, forKey: .attributes) - try container.encodeIfPresent(incidents, forKey: .incidents) - try container.encodeIfPresent(closures, forKey: .closures) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/ResponseDisposition.swift b/ios/Classes/Navigation/MapboxDirections/ResponseDisposition.swift deleted file mode 100644 index 5c46e9892..000000000 --- a/ios/Classes/Navigation/MapboxDirections/ResponseDisposition.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -struct ResponseDisposition: Decodable, Equatable { - var code: String? - var message: String? - var error: String? - - private enum CodingKeys: CodingKey { - case code, message, error - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/RestStop.swift b/ios/Classes/Navigation/MapboxDirections/RestStop.swift deleted file mode 100644 index e43cd94b0..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RestStop.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Foundation -import Turf - -/// A [rest stop](https://wiki.openstreetmap.org/wiki/Tag:highway%3Drest_area) along the route. -public struct RestStop: Codable, Equatable, ForeignMemberContainer, Sendable { - public var foreignMembers: JSONObject = [:] - - /// A kind of rest stop. - public enum StopType: String, Codable, Sendable { - /// A primitive rest stop that provides parking but no additional services. - case serviceArea = "service_area" - /// A major rest stop that provides amenities such as fuel and food. - case restArea = "rest_area" - } - - /// The kind of the rest stop. - public let type: StopType - - /// The name of the rest stop, if available. - public let name: String? - - /// Facilities associated with the rest stop, if available. - public let amenities: [Amenity]? - - private enum CodingKeys: String, CodingKey { - case type - case name - case amenities - } - - /// Initializes an unnamed rest stop of a certain kind. - /// - /// - Parameter type: The kind of rest stop. - public init(type: StopType) { - self.type = type - self.name = nil - self.amenities = nil - } - - /// Initializes an optionally named rest stop of a certain kind. - /// - Parameters: - /// - type: The kind of rest stop. - /// - name: The name of the rest stop. - /// - amenities: Facilities associated with the rest stop. - public init(type: StopType, name: String?, amenities: [Amenity]?) { - self.type = type - self.name = name - self.amenities = amenities - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.type = try container.decode(StopType.self, forKey: .type) - self.name = try container.decodeIfPresent(String.self, forKey: .name) - self.amenities = try container.decodeIfPresent([Amenity].self, forKey: .amenities) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type, forKey: .type) - try container.encodeIfPresent(name, forKey: .name) - try container.encodeIfPresent(amenities, forKey: .amenities) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - public static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.type == rhs.type - && lhs.name == rhs.name - && lhs.amenities == rhs.amenities - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/RoadClassExclusionViolation.swift b/ios/Classes/Navigation/MapboxDirections/RoadClassExclusionViolation.swift deleted file mode 100644 index 5afef5371..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RoadClassExclusionViolation.swift +++ /dev/null @@ -1,16 +0,0 @@ - -import Foundation - -/// Exact ``RoadClasses`` exclusion violation case. -public struct RoadClassExclusionViolation: Equatable, Sendable { - /// ``RoadClasses`` that were violated at this point. - public var roadClasses: RoadClasses - /// Index of a ``Route`` inside ``RouteResponse`` where violation occured. - public var routeIndex: Int - /// Index of a ``RouteLeg`` inside ``Route`` where violation occured. - public var legIndex: Int - /// Index of a ``RouteStep`` inside ``RouteLeg`` where violation occured. - public var stepIndex: Int - /// Index of an `Intersection` inside ``RouteStep`` where violation occured. - public var intersectionIndex: Int -} diff --git a/ios/Classes/Navigation/MapboxDirections/RoadClasses.swift b/ios/Classes/Navigation/MapboxDirections/RoadClasses.swift deleted file mode 100644 index cec282186..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RoadClasses.swift +++ /dev/null @@ -1,175 +0,0 @@ -import Foundation - -/// Option set that contains attributes of a road segment. -public struct RoadClasses: OptionSet, CustomStringConvertible, Sendable, Equatable { - public var rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - /// The road segment is [tolled](https://wiki.openstreetmap.org/wiki/Key:toll). - /// - /// This option can only be used with ``RouteOptions/roadClassesToAvoid``. - public static let toll = RoadClasses(rawValue: 1 << 1) - - /// The road segment has access restrictions. - /// - /// A road segment may have this class if there are [general access - /// restrictions](https://wiki.openstreetmap.org/wiki/Key:access) or a [high-occupancy - /// vehicle](https://wiki.openstreetmap.org/wiki/Key:hov) restriction. - /// - /// This option **cannot** be used with ``RouteOptions/roadClassesToAvoid`` or ``RouteOptions/roadClassesToAllow``. - public static let restricted = RoadClasses(rawValue: 1 << 2) - - /// The road segment is a [freeway](https://wiki.openstreetmap.org/wiki/Tag:highway%3Dmotorway) or [freeway - /// ramp](https://wiki.openstreetmap.org/wiki/Tag:highway%3Dmotorway_link). - /// - /// It may be desirable to suppress the name of the freeway when giving instructions and give instructions at fixed - /// distances before an exit (such as 1 mile or 1 kilometer ahead). - /// - /// This option can only be used with ``RouteOptions/roadClassesToAvoid``. - public static let motorway = RoadClasses(rawValue: 1 << 3) - - /// The user must travel this segment of the route by ferry. - /// - /// The user should verify that the ferry is in operation. For driving and cycling directions, the user should also - /// verify that their vehicle is permitted onboard the ferry. - /// - /// In general, the transport type of the step containing the road segment is also ``TransportType/ferry``. - /// - /// This option can only be used with ``RouteOptions/roadClassesToAvoid``. - public static let ferry = RoadClasses(rawValue: 1 << 4) - - /// The user must travel this segment of the route through a - /// [tunnel](https://wiki.openstreetmap.org/wiki/Key:tunnel). - /// - /// This option **cannot** be used with ``RouteOptions/roadClassesToAvoid`` or ``RouteOptions/roadClassesToAllow``. - public static let tunnel = RoadClasses(rawValue: 1 << 5) - - /// The road segment is a [high occupancy vehicle road](https://wiki.openstreetmap.org/wiki/Key:hov) that requires a - /// minimum of two vehicle occupants. - /// - /// This option includes high occupancy vehicle road segments that require a minimum of two vehicle occupants only, - /// not high occupancy vehicle lanes. - /// If the user is in a high-occupancy vehicle with two occupants and would accept a route that uses a [high - /// occupancy toll road](https://wikipedia.org/wiki/High-occupancy_toll_lane), specify both - /// ``RoadClasses/highOccupancyVehicle2`` and ``RoadClasses/highOccupancyToll``. Otherwise, the routes will avoid - /// any road that requires anyone to pay a toll. - /// - /// This option can only be used with ``RouteOptions/roadClassesToAllow``. - public static let highOccupancyVehicle2 = RoadClasses(rawValue: 1 << 6) - - /// The road segment is a [high occupancy vehicle road](https://wiki.openstreetmap.org/wiki/Key:hov) that requires a - /// minimum of three vehicle occupants. - /// - /// This option includes high occupancy vehicle road segments that require a minimum of three vehicle occupants - /// only, not high occupancy vehicle lanes. - /// - /// This option can only be used with ``RouteOptions/roadClassesToAllow``. - public static let highOccupancyVehicle3 = RoadClasses(rawValue: 1 << 7) - - /// The road segment is a [high occupancy toll road](https://wikipedia.org/wiki/High-occupancy_toll_lane) that is - /// tolled if the user's vehicle does not meet the minimum occupant requirement. - /// - /// This option includes high occupancy toll road segments only, not high occupancy toll lanes. - /// - /// This option can only be used with ``RouteOptions/roadClassesToAllow``. - public static let highOccupancyToll = RoadClasses(rawValue: 1 << 8) - - /// The user must travel this segment of the route on an unpaved road. - /// - /// This option can only be used with ``RouteOptions/roadClassesToAvoid``. - public static let unpaved = RoadClasses(rawValue: 1 << 9) - - /// The road segment is [tolled](https://wiki.openstreetmap.org/wiki/Key:toll) and only accepts cash payment. - /// - /// This option can only be used with ``RouteOptions/roadClassesToAvoid``. - public static let cashTollOnly = RoadClasses(rawValue: 1 << 10) - - /// Creates a ``RoadClasses`` given an array of strings. - public init?(descriptions: [String]) { - var roadClasses: RoadClasses = [] - for description in descriptions { - switch description { - case "toll": - roadClasses.insert(.toll) - case "restricted": - roadClasses.insert(.restricted) - case "motorway": - roadClasses.insert(.motorway) - case "ferry": - roadClasses.insert(.ferry) - case "tunnel": - roadClasses.insert(.tunnel) - case "hov2": - roadClasses.insert(.highOccupancyVehicle2) - case "hov3": - roadClasses.insert(.highOccupancyVehicle3) - case "hot": - roadClasses.insert(.highOccupancyToll) - case "unpaved": - roadClasses.insert(.unpaved) - case "cash_only_tolls": - roadClasses.insert(.cashTollOnly) - case "": - continue - default: - return nil - } - } - self.init(rawValue: roadClasses.rawValue) - } - - public var description: String { - var descriptions: [String] = [] - if contains(.toll) { - descriptions.append("toll") - } - if contains(.restricted) { - descriptions.append("restricted") - } - if contains(.motorway) { - descriptions.append("motorway") - } - if contains(.ferry) { - descriptions.append("ferry") - } - if contains(.tunnel) { - descriptions.append("tunnel") - } - if contains(.highOccupancyVehicle2) { - descriptions.append("hov2") - } - if contains(.highOccupancyVehicle3) { - descriptions.append("hov3") - } - if contains(.highOccupancyToll) { - descriptions.append("hot") - } - if contains(.unpaved) { - descriptions.append("unpaved") - } - if contains(.cashTollOnly) { - descriptions.append("cash_only_tolls") - } - return descriptions.joined(separator: ",") - } -} - -extension RoadClasses: Codable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(description.components(separatedBy: ",").filter { !$0.isEmpty }) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let descriptions = try container.decode([String].self) - if let roadClasses = RoadClasses(descriptions: descriptions) { - self = roadClasses - } else { - throw DirectionsError.invalidResponse(nil) - } - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Route.swift b/ios/Classes/Navigation/MapboxDirections/Route.swift deleted file mode 100644 index 1c98e4a11..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Route.swift +++ /dev/null @@ -1,130 +0,0 @@ -import Foundation -import Turf - -/// A ``Route`` object defines a single route that the user can follow to visit a series of waypoints in order. The -/// route object includes information about the route, such as its distance and expected travel time. Depending on the -/// criteria used to calculate the route, the route object may also include detailed turn-by-turn instructions. -/// -/// Typically, you do not create instances of this class directly. Instead, you receive route objects when you request -/// directions using the `Directions.calculate(_:completionHandler:)` or -/// `Directions.calculateRoutes(matching:completionHandler:)` method. However, if you use the -/// `Directions.url(forCalculating:)` method instead, you can use `JSONDecoder` to convert the HTTP response into a -/// ``RouteResponse`` or ``MapMatchingResponse`` object and access the ``RouteResponse/routes`` or -/// ``MapMatchingResponse/matches`` property. -public struct Route: DirectionsResult { - public enum CodingKeys: String, CodingKey, CaseIterable { - case tollPrices = "toll_costs" - } - - public var shape: Turf.LineString? - - public var legs: [RouteLeg] - - public var distance: Turf.LocationDistance - - public var expectedTravelTime: TimeInterval - - public var typicalTravelTime: TimeInterval? - - public var speechLocale: Locale? - - public var fetchStartDate: Date? - - public var responseEndDate: Date? - - public var responseContainsSpeechLocale: Bool - - public var foreignMembers: Turf.JSONObject = [:] - - /// Initializes a route. - /// - Parameters: - /// - legs: The legs that are traversed in order. - /// - shape: The roads or paths taken as a contiguous polyline. - /// - distance: The route’s distance, measured in meters. - /// - expectedTravelTime: The route’s expected travel time, measured in seconds. - /// - typicalTravelTime: The route’s typical travel time, measured in seconds. - public init( - legs: [RouteLeg], - shape: LineString?, - distance: LocationDistance, - expectedTravelTime: TimeInterval, - typicalTravelTime: TimeInterval? = nil - ) { - self.legs = legs - self.shape = shape - self.distance = distance - self.expectedTravelTime = expectedTravelTime - self.typicalTravelTime = typicalTravelTime - self.responseContainsSpeechLocale = false - } - - /// Initializes a route from a decoder. - /// - /// - Precondition: If the decoder is decoding JSON data from an API response, the `Decoder.userInfo` dictionary - /// must contain a ``RouteOptions`` or ``MatchOptions`` object in the ``Swift/CodingUserInfoKey/options`` key. If it - /// does not, a ``DirectionsCodingError/missingOptions`` error is thrown. - /// - Parameter decoder: The decoder of JSON-formatted API response data or a previously encoded ``Route`` object. - public init(from decoder: Decoder) throws { - guard let options = decoder.userInfo[.options] as? DirectionsOptions else { - throw DirectionsCodingError.missingOptions - } - - let container = try decoder.container(keyedBy: DirectionsCodingKey.self) - self.tollPrices = try container.decodeIfPresent([TollPriceCoder].self, forKey: .route(.tollPrices))? - .reduce(into: []) { $0.append(contentsOf: $1.tollPrices) } - self.legs = try Self.decodeLegs(using: container, options: options) - self.distance = try Self.decodeDistance(using: container) - self.expectedTravelTime = try Self.decodeExpectedTravelTime(using: container) - self.typicalTravelTime = try Self.decodeTypicalTravelTime(using: container) - self.shape = try Self.decodeShape(using: container) - self.speechLocale = try Self.decodeSpeechLocale(using: container) - self.responseContainsSpeechLocale = try Self.decodeResponseContainsSpeechLocale(using: container) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: DirectionsCodingKey.self) - try container.encodeIfPresent(tollPrices.map { TollPriceCoder(tollPrices: $0) }, forKey: .route(.tollPrices)) - - try encodeLegs(into: &container) - try encodeShape(into: &container, options: encoder.userInfo[.options] as? DirectionsOptions) - try encodeDistance(into: &container) - try encodeExpectedTravelTime(into: &container) - try encodeTypicalTravelTime(into: &container) - try encodeSpeechLocale(into: &container) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - /// List of calculated toll costs for this route. - /// - /// This property is set to `nil` unless request ``RouteOptions/includesTollPrices`` is set to `true`. - public var tollPrices: [TollPrice]? -} - -extension Route: CustomStringConvertible { - public var description: String { - return legs.map(\.name).joined(separator: " – ") - } -} - -extension DirectionsCodingKey { - static func route(_ key: Route.CodingKeys) -> Self { - .init(stringValue: key.rawValue) - } -} - -extension Route: Equatable { - public static func == (lhs: Route, rhs: Route) -> Bool { - return lhs.distance == rhs.distance && - lhs.expectedTravelTime == rhs.expectedTravelTime && - lhs.typicalTravelTime == rhs.typicalTravelTime && - lhs.speechLocale == rhs.speechLocale && - lhs.responseContainsSpeechLocale == rhs.responseContainsSpeechLocale && - lhs.legs == rhs.legs && - lhs.shape == rhs.shape && - lhs.tollPrices.map { Set($0) } == rhs.tollPrices - .map { Set($0) } // comparing sets to mitigate items reordering caused by custom Coding impl. - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteLeg.swift b/ios/Classes/Navigation/MapboxDirections/RouteLeg.swift deleted file mode 100644 index ef1e88499..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RouteLeg.swift +++ /dev/null @@ -1,549 +0,0 @@ -import Foundation -import Turf - -/// A ``RouteLeg`` object defines a single leg of a route between two waypoints. If the overall route has only two -/// waypoints, it has a single ``RouteLeg`` object that covers the entire route. The route leg object includes -/// information about the leg, such as its name, distance, and expected travel time. Depending on the criteria used to -/// calculate the route, the route leg object may also include detailed turn-by-turn instructions. -/// -/// You do not create instances of this class directly. Instead, you receive route leg objects as part of route objects -/// when you request directions using the `Directions.calculate(_:completionHandler:)` method. -public struct RouteLeg: Codable, ForeignMemberContainer, Equatable, Sendable { - public var foreignMembers: JSONObject = [:] - - /// Foreign attribute arrays associated with this leg. - /// - /// This library does not officially support any attribute that is documented as a “beta” annotation type in the - /// Mapbox Directions API response format, but you can get and set it as an element of this `JSONObject`. It is - /// round-tripped to the `annotation` property in JSON. - /// - /// For non-attribute-related foreign members, use the ``foreignMembers`` property. - public var attributesForeignMembers: JSONObject = [:] - - public enum CodingKeys: String, CodingKey, CaseIterable { - case source - case destination - case steps - case name = "summary" - case distance - case expectedTravelTime = "duration" - case typicalTravelTime = "duration_typical" - case profileIdentifier - case annotation - case administrativeRegions = "admins" - case incidents - case viaWaypoints = "via_waypoints" - case closures - } - - // MARK: Creating a Leg - - /// Initializes a route leg. - /// - Parameters: - /// - steps: The steps that are traversed in order. - /// - name: A name that describes the route leg. - /// - distance: The route leg’s expected travel time, measured in seconds. - /// - expectedTravelTime: The route leg’s expected travel time, measured in seconds. - /// - typicalTravelTime: The route leg’s typical travel time, measured in seconds. - /// - profileIdentifier: The primary mode of transportation for the route leg. - public init( - steps: [RouteStep], - name: String, - distance: Turf.LocationDistance, - expectedTravelTime: TimeInterval, - typicalTravelTime: TimeInterval? = nil, - profileIdentifier: ProfileIdentifier - ) { - self.steps = steps - self.name = name - self.distance = distance - self.expectedTravelTime = expectedTravelTime - self.typicalTravelTime = typicalTravelTime - self.profileIdentifier = profileIdentifier - - self.segmentDistances = nil - self.expectedSegmentTravelTimes = nil - self.segmentSpeeds = nil - self.segmentCongestionLevels = nil - self.segmentNumericCongestionLevels = nil - } - - /// Creates a route leg from a decoder. - /// - Precondition: If the decoder is decoding JSON data from an API response, the `Decoder.userInfo` dictionary - /// must contain a ``RouteOptions`` or ``MatchOptions`` object in the ``Swift/CodingUserInfoKey/options`` key. If it - /// does not, a ``DirectionsCodingError/missingOptions`` error is thrown. - /// - parameter decoder: The decoder of JSON-formatted API response data or a previously encoded ``RouteLeg`` - /// object. - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.source = try container.decodeIfPresent(Waypoint.self, forKey: .source) - self.destination = try container.decodeIfPresent(Waypoint.self, forKey: .destination) - self.name = try container.decode(String.self, forKey: .name) - self.distance = try container.decode(Turf.LocationDistance.self, forKey: .distance) - self.expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime) - self.typicalTravelTime = try container.decodeIfPresent(TimeInterval.self, forKey: .typicalTravelTime) - - if let profileIdentifier = try container.decodeIfPresent(ProfileIdentifier.self, forKey: .profileIdentifier) { - self.profileIdentifier = profileIdentifier - } else if let options = decoder.userInfo[.options] as? DirectionsOptions { - self.profileIdentifier = options.profileIdentifier - } else { - throw DirectionsCodingError.missingOptions - } - - if let admins = try container.decodeIfPresent([AdministrativeRegion].self, forKey: .administrativeRegions) { - self.administrativeRegions = admins - self.steps = try RouteStep.decode( - from: container.superDecoder(forKey: .steps), - administrativeRegions: administrativeRegions! - ) - } else { - self.steps = try container.decode([RouteStep].self, forKey: .steps) - } - - if let attributes = try container.decodeIfPresent(Attributes.self, forKey: .annotation) { - self.attributes = attributes - self.attributesForeignMembers = attributes.foreignMembers - } - - if let incidents = try container.decodeIfPresent([Incident].self, forKey: .incidents) { - self.incidents = incidents - } - - if let closures = try container.decodeIfPresent([Closure].self, forKey: .closures) { - self.closures = closures - } - - if let viaWaypoints = try container.decodeIfPresent([SilentWaypoint].self, forKey: .viaWaypoints) { - self.viaWaypoints = viaWaypoints - } - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(source, forKey: .source) - try container.encode(destination, forKey: .destination) - try container.encode(steps, forKey: .steps) - try container.encode(name, forKey: .name) - try container.encode(distance, forKey: .distance) - try container.encode(expectedTravelTime, forKey: .expectedTravelTime) - try container.encodeIfPresent(typicalTravelTime, forKey: .typicalTravelTime) - try container.encode(profileIdentifier, forKey: .profileIdentifier) - - var attributes = attributes - if !attributes.isEmpty { - attributes.foreignMembers = attributesForeignMembers - try container.encode(attributes, forKey: .annotation) - } - - if let admins = administrativeRegions { - try container.encode(admins, forKey: .administrativeRegions) - } - - if let incidents { - try container.encode(incidents, forKey: .incidents) - } - if let closures { - try container.encode(closures, forKey: .closures) - } - - if let viaWaypoints { - try container.encode(viaWaypoints, forKey: .viaWaypoints) - } - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - // MARK: Getting the Endpoints of the Leg - - /// The starting point of the route leg. - /// - /// Unless this is the first leg of the route, the source of this leg is the same as the destination of the previous - /// leg. - /// - /// This property is set to `nil` if the leg was decoded from a JSON RouteLeg object. - public var source: Waypoint? - - /// The endpoint of the route leg. - /// - /// Unless this is the last leg of the route, the destination of this leg is the same as the source of the next leg. - /// - /// This property is set to `nil` if the leg was decoded from a JSON RouteLeg object. - public var destination: Waypoint? - - // MARK: Getting the Steps Along the Leg - - /// An array of one or more ``RouteStep`` objects representing the steps for traversing this leg of the route. - /// - /// Each route step object corresponds to a distinct maneuver and the approach to the next maneuver. - /// - /// This array is empty if the original ``RouteOptions`` object’s ``DirectionsOptions/includesSteps`` property is - /// set to - /// `false`. - public let steps: [RouteStep] - - /// The ranges of each step’s segments within the overall leg. - /// - /// Each range corresponds to an element of the ``steps`` property. Use this property to safely subscript - /// segment-based properties such as ``segmentCongestionLevels`` and ``segmentMaximumSpeedLimits``. - /// - /// This array is empty if the original ``RouteOptions`` object’s ``DirectionsOptions/includesSteps`` property is - /// set to - /// `false`. - public private(set) lazy var segmentRangesByStep: [Range] = { - var segmentRangesByStep: [Range] = [] - var currentStepStartIndex = 0 - for step in steps { - if let coordinates = step.shape?.coordinates { - let stepCoordinateCount = step.maneuverType == .arrive ? 0 : coordinates.dropLast().count - let currentStepEndIndex = currentStepStartIndex.advanced(by: stepCoordinateCount) - segmentRangesByStep.append(currentStepStartIndex..?]? - - /// An array of ``RouteLeg/Closure`` objects describing live-traffic related closures that occur along the route. - /// - /// This information is only available for the `mapbox/driving-traffic` profile and when - /// ``DirectionsOptions/attributeOptions`` property contains ``AttributeOptions/closures``. - public var closures: [Closure]? - - /// The tendency value conveys the changing state of traffic congestion (increasing, decreasing, constant etc). - public var trafficTendencies: [TrafficTendency]? - - /// The full collection of attributes along the leg. - var attributes: Attributes { - get { - return Attributes( - segmentDistances: segmentDistances, - expectedSegmentTravelTimes: expectedSegmentTravelTimes, - segmentSpeeds: segmentSpeeds, - segmentCongestionLevels: segmentCongestionLevels, - segmentNumericCongestionLevels: segmentNumericCongestionLevels, - segmentMaximumSpeedLimits: segmentMaximumSpeedLimits, - trafficTendencies: trafficTendencies - ) - } - set { - segmentDistances = newValue.segmentDistances - expectedSegmentTravelTimes = newValue.expectedSegmentTravelTimes - segmentSpeeds = newValue.segmentSpeeds - segmentCongestionLevels = newValue.segmentCongestionLevels - segmentNumericCongestionLevels = newValue.segmentNumericCongestionLevels - segmentMaximumSpeedLimits = newValue.segmentMaximumSpeedLimits - trafficTendencies = newValue.trafficTendencies - } - } - - mutating func refreshAttributes(newAttributes: Attributes, startLegShapeIndex: Int = 0) { - let refreshRange = PartialRangeFrom(startLegShapeIndex) - - segmentDistances?.replaceIfPossible(subrange: refreshRange, with: newAttributes.segmentDistances) - expectedSegmentTravelTimes?.replaceIfPossible( - subrange: refreshRange, - with: newAttributes.expectedSegmentTravelTimes - ) - segmentSpeeds?.replaceIfPossible(subrange: refreshRange, with: newAttributes.segmentSpeeds) - segmentCongestionLevels?.replaceIfPossible(subrange: refreshRange, with: newAttributes.segmentCongestionLevels) - segmentNumericCongestionLevels?.replaceIfPossible( - subrange: refreshRange, - with: newAttributes.segmentNumericCongestionLevels - ) - segmentMaximumSpeedLimits?.replaceIfPossible( - subrange: refreshRange, - with: newAttributes.segmentMaximumSpeedLimits - ) - trafficTendencies?.replaceIfPossible(subrange: refreshRange, with: newAttributes.trafficTendencies) - } - - private func adjustShapeIndexRange(_ range: Range, startLegShapeIndex: Int) -> Range { - let startIndex = startLegShapeIndex + range.lowerBound - let endIndex = startLegShapeIndex + range.upperBound - return startIndex.. String? { - // check index ranges - guard let administrativeRegions, - stepIndex < steps.count, - intersectionIndex < steps[stepIndex].administrativeAreaContainerByIntersection?.count ?? -1, - let adminIndex = steps[stepIndex].administrativeAreaContainerByIntersection?[intersectionIndex] - else { - return nil - } - return administrativeRegions[adminIndex].countryCode - } - - // MARK: Getting Statistics About the Leg - - /// A name that describes the route leg. - /// - /// The name describes the leg using the most significant roads or trails along the route leg. You can display this - /// string to the user to help the user can distinguish one route from another based on how the legs of the routes - /// are named. - /// - /// The leg’s name does not identify the start and end points of the leg. To distinguish one leg from another within - /// the same route, concatenate the ``name`` properties of the ``source`` and ``destination`` waypoints. - public let name: String - - /// The route leg’s distance, measured in meters. - /// - /// The value of this property accounts for the distance that the user must travel to arrive at the destination from - /// the source. It is not the direct distance between the source and destination, nor should not assume that the - /// user would travel along this distance at a fixed speed. - public let distance: Turf.LocationDistance - - /// The route leg’s expected travel time, measured in seconds. - /// - /// The value of this property reflects the time it takes to traverse the route leg. If the route was calculated - /// using the ``ProfileIdentifier/automobileAvoidingTraffic`` profile, this property reflects current traffic - /// conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin - /// this leg. For other profiles, this property reflects travel time under ideal conditions and does not account for - /// traffic congestion. If the leg makes use of a ferry or train, the actual travel time may additionally be subject - /// to the schedules of those services. - /// - /// Do not assume that the user would travel along the leg at a fixed speed. For the expected travel time on each - /// individual segment along the leg, use the ``RouteStep/expectedTravelTime`` property. For more granularity, - /// specify the ``AttributeOptions/expectedTravelTime`` option and use the ``expectedSegmentTravelTimes`` property. - public var expectedTravelTime: TimeInterval - - /// The administrative regions through which the leg passes. - /// - /// Items are ordered by appearance, most recent one is at the beginning. This property is set to `nil` if no - /// administrative region data is available. - /// You can alse refer to ``Intersection/regionCode`` to get corresponding region string code. - public var administrativeRegions: [AdministrativeRegion]? - - /// Contains ``Incident``s data which occur during current ``RouteLeg``. - /// - /// Items are ordered by appearance, most recent one is at the beginning. - /// This property is set to `nil` if incidents data is not available. - public var incidents: [Incident]? - - /// Describes where a particular ``Waypoint`` passed to ``RouteOptions`` matches to the route along a ``RouteLeg``. - /// - /// The property is non-nil when for one or several ``Waypoint`` objects passed to ``RouteOptions`` have - /// ``Waypoint/separatesLegs`` property set to `false`. - public var viaWaypoints: [SilentWaypoint]? - - /// The route leg’s typical travel time, measured in seconds. - /// - /// The value of this property reflects the typical time it takes to traverse the route leg. This property is - /// available when using the ``ProfileIdentifier/automobileAvoidingTraffic`` profile. This property reflects typical - /// traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the - /// user would begin this leg. If the leg makes use of a ferry, the typical travel time may additionally be subject - /// to the schedule of this service. - /// - /// Do not assume that the user would travel along the route at a fixed speed. For more granular typical travel - /// times, use the ``RouteStep/typicalTravelTime`` property. - public var typicalTravelTime: TimeInterval? - - // MARK: Reproducing the Route - - /// The primary mode of transportation for the route leg. - /// - /// The value of this property depends on the ``DirectionsOptions/profileIdentifier`` property of the original - /// ``RouteOptions`` object. This property reflects the primary mode of transportation used for the route leg. - /// Individual steps along the route leg might use different modes of transportation as necessary. - public let profileIdentifier: ProfileIdentifier -} - -extension RouteLeg: CustomStringConvertible { - public var description: String { - return name - } -} - -extension RouteLeg: CustomQuickLookConvertible { - func debugQuickLookObject() -> Any? { - let coordinates = steps.reduce([]) { $0 + ($1.shape?.coordinates ?? []) } - guard !coordinates.isEmpty else { - return nil - } - return debugQuickLookURL(illustrating: LineString(coordinates)) - } -} - -extension RouteLeg { - /// Live-traffic related closure that occured along the route. - public struct Closure: Codable, Equatable, ForeignMemberContainer, Sendable { - public var foreignMembers: JSONObject = [:] - - private enum CodingKeys: String, CodingKey { - case geometryIndexStart = "geometry_index_start" - case geometryIndexEnd = "geometry_index_end" - } - - /// The range of segments within the current leg, where the closure spans. - public var shapeIndexRange: Range - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let geometryIndexStart = try container.decode(Int.self, forKey: .geometryIndexStart) - let geometryIndexEnd = try container.decode(Int.self, forKey: .geometryIndexEnd) - self.shapeIndexRange = geometryIndexStart.., with newElements: Array?) { - guard let newElements, !newElements.isEmpty else { return } - let upperBound = subrange.lowerBound + newElements.count - - guard count >= upperBound else { return } - - let adjustedSubrange = subrange.lowerBound.. Bool { - return lhs.source == rhs.source && - lhs.destination == rhs.destination && - lhs.steps == rhs.steps && - lhs.segmentDistances == rhs.segmentDistances && - lhs.expectedSegmentTravelTimes == rhs.expectedSegmentTravelTimes && - lhs.segmentSpeeds == rhs.segmentSpeeds && - lhs.segmentCongestionLevels == rhs.segmentCongestionLevels && - lhs.segmentNumericCongestionLevels == rhs.segmentNumericCongestionLevels && - lhs.segmentMaximumSpeedLimits == rhs.segmentMaximumSpeedLimits && - lhs.trafficTendencies == rhs.trafficTendencies && - lhs.name == rhs.name && - lhs.distance == rhs.distance && - lhs.expectedTravelTime == rhs.expectedTravelTime && - lhs.administrativeRegions == rhs.administrativeRegions && - lhs.incidents == rhs.incidents && - lhs.viaWaypoints == rhs.viaWaypoints && - lhs.typicalTravelTime == rhs.typicalTravelTime && - lhs.profileIdentifier == rhs.profileIdentifier - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteLegAttributes.swift b/ios/Classes/Navigation/MapboxDirections/RouteLegAttributes.swift deleted file mode 100644 index 55733321c..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RouteLegAttributes.swift +++ /dev/null @@ -1,147 +0,0 @@ -import Foundation -import Turf - -extension RouteLeg { - /// A collection of per-segment attributes along a route leg. - public struct Attributes: Equatable, ForeignMemberContainer { - public var foreignMembers: JSONObject = [:] - - /// An array containing the distance (measured in meters) between each coordinate in the route leg geometry. - /// - /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains - /// ``AttributeOptions/distance``. - public var segmentDistances: [LocationDistance]? - - /// An array containing the expected travel time (measured in seconds) between each coordinate in the route leg - /// geometry. - /// - /// These values are dynamic, accounting for any conditions that may change along a segment, such as traffic - /// congestion if the profile identifier is set to ``ProfileIdentifier/automobileAvoidingTraffic`. - /// - /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains - /// `AttributeOptions.expectedTravelTime`. - public var expectedSegmentTravelTimes: [TimeInterval]? - - /// An array containing the expected average speed (measured in meters per second) between each coordinate in - /// the route leg geometry. - /// - /// These values are dynamic; rather than speed limits, they account for the road’s classification and/or any - /// traffic congestion (if the profile identifier is set to ``ProfileIdentifier/automobileAvoidingTraffic`). - /// - /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains - /// ``AttributeOptions/speed``. - public var segmentSpeeds: [LocationSpeed]? - - /// An array containing the traffic congestion level along each road segment in the route leg geometry. - /// - /// Traffic data is available in [a number of countries and territories - /// worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/#traffic-data). - /// - /// You can color-code a route line according to the congestion level along each segment of the route. - /// - /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains - /// ``AttributeOptions/congestionLevel``. - public var segmentCongestionLevels: [CongestionLevel]? - - /// An array containing the traffic congestion level along each road segment in the route leg geometry. - /// - /// Traffic data is available in [a number of countries and territories - /// worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/#traffic-data). - /// - /// You can color-code a route line according to the congestion level along each segment of the route. - /// - /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains - /// ``AttributeOptions/numericCongestionLevel``. - public var segmentNumericCongestionLevels: [NumericCongestionLevel?]? - - /// An array containing the maximum speed limit along each road segment along the route leg’s shape. - /// - /// The maximum speed may be an advisory speed limit for segments where legal limits are not posted, such as - /// highway entrance and exit ramps. If the speed limit along a particular segment is unknown, it is represented - /// in the array by a measurement whose value is negative. If the speed is unregulated along the segment, such - /// as on the German _Autobahn_ system, it is represented by a measurement whose value is `Double.infinity`. - /// - /// Speed limit data is available in [a number of countries and territories - /// worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/). - /// - /// This property is set if the ``DirectionsOptions/attributeOptions`` property contains - /// ``AttributeOptions/maximumSpeedLimit``. - public var segmentMaximumSpeedLimits: [Measurement?]? - - /// The tendency value conveys the changing state of traffic congestion (increasing, decreasing, constant etc). - public var trafficTendencies: [TrafficTendency]? - } -} - -extension RouteLeg.Attributes: Codable { - private enum CodingKeys: String, CodingKey { - case segmentDistances = "distance" - case expectedSegmentTravelTimes = "duration" - case segmentSpeeds = "speed" - case segmentCongestionLevels = "congestion" - case segmentNumericCongestionLevels = "congestion_numeric" - case segmentMaximumSpeedLimits = "maxspeed" - case trafficTendencies = "traffic_tendency" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - segmentDistances = try container.decodeIfPresent([LocationDistance].self, forKey: .segmentDistances) - expectedSegmentTravelTimes = try container.decodeIfPresent( - [TimeInterval].self, - forKey: .expectedSegmentTravelTimes - ) - segmentSpeeds = try container.decodeIfPresent([LocationSpeed].self, forKey: .segmentSpeeds) - segmentCongestionLevels = try container.decodeIfPresent( - [CongestionLevel].self, - forKey: .segmentCongestionLevels - ) - segmentNumericCongestionLevels = try container.decodeIfPresent( - [NumericCongestionLevel?].self, - forKey: .segmentNumericCongestionLevels - ) - - if let speedLimitDescriptors = try container.decodeIfPresent( - [SpeedLimitDescriptor].self, - forKey: .segmentMaximumSpeedLimits - ) { - segmentMaximumSpeedLimits = speedLimitDescriptors.map { Measurement(speedLimitDescriptor: $0) } - } else { - segmentMaximumSpeedLimits = nil - } - - trafficTendencies = try container.decodeIfPresent([TrafficTendency].self, forKey: .trafficTendencies) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(segmentDistances, forKey: .segmentDistances) - try container.encodeIfPresent(expectedSegmentTravelTimes, forKey: .expectedSegmentTravelTimes) - try container.encodeIfPresent(segmentSpeeds, forKey: .segmentSpeeds) - try container.encodeIfPresent(segmentCongestionLevels, forKey: .segmentCongestionLevels) - try container.encodeIfPresent(segmentNumericCongestionLevels, forKey: .segmentNumericCongestionLevels) - - if let speedLimitDescriptors = segmentMaximumSpeedLimits?.map({ SpeedLimitDescriptor(speed: $0) }) { - try container.encode(speedLimitDescriptors, forKey: .segmentMaximumSpeedLimits) - } - - try container.encodeIfPresent(trafficTendencies, forKey: .trafficTendencies) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - /// Returns whether any attributes are non-nil. - var isEmpty: Bool { - return segmentDistances == nil && - expectedSegmentTravelTimes == nil && - segmentSpeeds == nil && - segmentCongestionLevels == nil && - segmentNumericCongestionLevels == nil && - segmentMaximumSpeedLimits == nil && - trafficTendencies == nil - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteOptions.swift b/ios/Classes/Navigation/MapboxDirections/RouteOptions.swift deleted file mode 100644 index c6f952358..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RouteOptions.swift +++ /dev/null @@ -1,657 +0,0 @@ -import Foundation -#if canImport(CoreLocation) -import CoreLocation -#endif -import Turf - -/// A ``RouteOptions`` object is a structure that specifies the criteria for results returned by the Mapbox Directions -/// API. -/// -/// Pass an instance of this class into the `Directions.calculate(_:completionHandler:)` method. -open class RouteOptions: DirectionsOptions, @unchecked Sendable { - // MARK: Creating a Route Options Object - - /// Initializes a route options object for routes between the given waypoints and an optional profile identifier. - /// - Parameters: - /// - waypoints: An array of ``Waypoint`` objects representing locations that the route should visit in - /// chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 - /// waypoints. (Some profiles, such as ``ProfileIdentifier/automobileAvoidingTraffic``, [may have lower - /// limits](https://www.mapbox.com/api-documentation/#directions).) - /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. - /// ``ProfileIdentifier/automobile`` is used by default. - /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. - public required init( - waypoints: [Waypoint], - profileIdentifier: ProfileIdentifier? = nil, - queryItems: [URLQueryItem]? = nil - ) { - let profilesDisallowingUTurns: [ProfileIdentifier] = [.automobile, .automobileAvoidingTraffic] - self.allowsUTurnAtWaypoint = !profilesDisallowingUTurns.contains(profileIdentifier ?? .automobile) - super.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) - - guard let queryItems else { - return - } - - let mappedQueryItems = [String: String]( - queryItems.compactMap { - guard let value = $0.value else { return nil } - return ($0.name, value) - }, - uniquingKeysWith: { _, latestValue in - return latestValue - } - ) - - if mappedQueryItems[CodingKeys.includesAlternativeRoutes.stringValue] == "true" { - self.includesAlternativeRoutes = true - } - if mappedQueryItems[CodingKeys.includesExitRoundaboutManeuver.stringValue] == "true" { - self.includesExitRoundaboutManeuver = true - } - if let mappedValue = mappedQueryItems[CodingKeys.alleyPriority.stringValue], - let alleyPriority = Double(mappedValue) - { - self.alleyPriority = DirectionsPriority(rawValue: alleyPriority) - } - if let mappedValue = mappedQueryItems[CodingKeys.walkwayPriority.stringValue], - let walkwayPriority = Double(mappedValue) - { - self.walkwayPriority = DirectionsPriority(rawValue: walkwayPriority) - } - if let mappedValue = mappedQueryItems[CodingKeys.speed.stringValue], - let speed = LocationSpeed(mappedValue) - { - self.speed = speed - } - if let mappedValue = mappedQueryItems[CodingKeys.roadClassesToAvoid.stringValue], - let roadClassesToAvoid = RoadClasses(descriptions: mappedValue.components(separatedBy: ",")) - { - self.roadClassesToAvoid = roadClassesToAvoid - } - if let mappedValue = mappedQueryItems[CodingKeys.roadClassesToAllow.stringValue], - let roadClassesToAllow = RoadClasses(descriptions: mappedValue.components(separatedBy: ",")) - { - self.roadClassesToAllow = roadClassesToAllow - } - if mappedQueryItems[CodingKeys.refreshingEnabled.stringValue] == "true", profileIdentifier == - .automobileAvoidingTraffic - { - self.refreshingEnabled = true - } - - // Making copy of waypoints processed by super class to further update them... - var waypoints = self.waypoints - if let mappedValue = mappedQueryItems[CodingKeys.waypointTargets.stringValue] { - var waypointsIndex = waypoints.startIndex - let mappedValues = mappedValue.components(separatedBy: ";") - var mappedValuesIndex = mappedValues.startIndex - - while waypointsIndex < waypoints.endIndex, - mappedValuesIndex < mappedValues.endIndex - { - guard waypoints[waypointsIndex].separatesLegs else { - waypointsIndex = waypoints.index(after: waypointsIndex); continue - } - - let coordinatesComponents = mappedValues[mappedValuesIndex].components(separatedBy: ",") - waypoints[waypointsIndex].targetCoordinate = LocationCoordinate2D( - latitude: LocationDegrees(coordinatesComponents.last!)!, - longitude: LocationDegrees(coordinatesComponents.first!)! - ) - waypointsIndex = waypoints.index(after: waypointsIndex) - mappedValuesIndex = mappedValues.index(after: mappedValuesIndex) - } - } - if let mappedValue = mappedQueryItems[CodingKeys.initialManeuverAvoidanceRadius.stringValue], - let initialManeuverAvoidanceRadius = LocationDistance(mappedValue) - { - self.initialManeuverAvoidanceRadius = initialManeuverAvoidanceRadius - } - if let mappedValue = mappedQueryItems[CodingKeys.maximumHeight.stringValue], - let doubleValue = Double(mappedValue) - { - self.maximumHeight = Measurement(value: doubleValue, unit: UnitLength.meters) - } - if let mappedValue = mappedQueryItems[CodingKeys.maximumWidth.stringValue], - let doubleValue = Double(mappedValue) - { - self.maximumWidth = Measurement(value: doubleValue, unit: UnitLength.meters) - } - if let mappedValue = mappedQueryItems[CodingKeys.maximumWeight.stringValue], - let doubleValue = Double(mappedValue) - { - self.maximumWeight = Measurement(value: doubleValue, unit: UnitMass.metricTons) - } - - if let mappedValue = mappedQueryItems[CodingKeys.layers.stringValue] { - let mappedValues = mappedValue.components(separatedBy: ";") - var waypointsIndex = waypoints.startIndex - for mappedValue in mappedValues { - guard waypointsIndex < waypoints.endIndex else { break } - waypoints[waypointsIndex].layer = Int(mappedValue) ?? nil - waypointsIndex = waypoints.index(after: waypointsIndex) - } - } - - let formatter = DateFormatter.ISO8601DirectionsFormatter() - if let mappedValue = mappedQueryItems[CodingKeys.departAt.stringValue], - let departAt = formatter.date(from: mappedValue) - { - self.departAt = departAt - } - if let mappedValue = mappedQueryItems[CodingKeys.arriveBy.stringValue], - let arriveBy = formatter.date(from: mappedValue) - { - self.arriveBy = arriveBy - } - if mappedQueryItems[CodingKeys.includesTollPrices.stringValue] == "true" { - self.includesTollPrices = true - } - self.waypoints = waypoints - } - -#if canImport(CoreLocation) - /// Initializes a route options object for routes between the given locations and an optional profile identifier. - /// - /// - Note: This initializer is intended for `CLLocation` objects created using the - /// `CLLocation.init(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a - /// `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid - /// overfitting, since the ``Waypoint`` class’s `coordinateAccuracy` property represents the maximum allowed - /// deviation from the waypoint. - /// - Parameters: - /// - locations: An array of `CLLocation` objects representing locations that the route should visit in - /// chronological order. The array should contain at least two locations (the source and destination) and at most 25 - /// locations. Each location object is converted into a ``Waypoint`` object. This class respects the `CLLocation` - /// class’s `coordinate` and `horizontalAccuracy` properties, converting them into the ``Waypoint`` class’s - /// ``Waypoint/coordinate`` and ``Waypoint/coordinateAccuracy`` properties, respectively. - /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. - /// ``ProfileIdentifier/automobile`` is used by default. - /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. - public convenience init( - locations: [CLLocation], - profileIdentifier: ProfileIdentifier? = nil, - queryItems: [URLQueryItem]? = nil - ) { - let waypoints = locations.map { Waypoint(location: $0) } - self.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) - } -#endif - - /// Initializes a route options object for routes between the given geographic coordinates and an optional profile - /// identifier. - /// - Parameters: - /// - coordinates: An array of geographic coordinates representing locations that the route should visit in - /// chronological order. The array should contain at least two locations (the source and destination) and at most 25 - /// locations. Each coordinate is converted into a ``Waypoint`` object. - /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. - /// ``ProfileIdentifier/automobile`` is used by default. - /// - queryItems: URL query items to be parsed and applied as configuration to the resulting options. - public convenience init( - coordinates: [LocationCoordinate2D], - profileIdentifier: ProfileIdentifier? = nil, - queryItems: [URLQueryItem]? = nil - ) { - let waypoints = coordinates.map { Waypoint(coordinate: $0) } - self.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems) - } - - private enum CodingKeys: String, CodingKey { - case allowsUTurnAtWaypoint = "continue_straight" - case includesAlternativeRoutes = "alternatives" - case includesExitRoundaboutManeuver = "roundabout_exits" - case roadClassesToAvoid = "exclude" - case roadClassesToAllow = "include" - case refreshingEnabled = "enable_refresh" - case initialManeuverAvoidanceRadius = "avoid_maneuver_radius" - case maximumHeight = "max_height" - case maximumWidth = "max_width" - case maximumWeight = "max_weight" - case alleyPriority = "alley_bias" - case walkwayPriority = "walkway_bias" - case speed = "walking_speed" - case waypointTargets = "waypoint_targets" - case arriveBy = "arrive_by" - case departAt = "depart_at" - case layers - case includesTollPrices = "compute_toll_cost" - } - - override public func encode(to encoder: Encoder) throws { - try super.encode(to: encoder) - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(allowsUTurnAtWaypoint, forKey: .allowsUTurnAtWaypoint) - try container.encode(includesAlternativeRoutes, forKey: .includesAlternativeRoutes) - try container.encode(includesExitRoundaboutManeuver, forKey: .includesExitRoundaboutManeuver) - try container.encode(roadClassesToAvoid, forKey: .roadClassesToAvoid) - try container.encode(roadClassesToAllow, forKey: .roadClassesToAllow) - try container.encode(refreshingEnabled, forKey: .refreshingEnabled) - try container.encodeIfPresent(initialManeuverAvoidanceRadius, forKey: .initialManeuverAvoidanceRadius) - try container.encodeIfPresent(maximumHeight?.converted(to: .meters).value, forKey: .maximumHeight) - try container.encodeIfPresent(maximumWidth?.converted(to: .meters).value, forKey: .maximumWidth) - try container.encodeIfPresent(maximumWeight?.converted(to: .metricTons).value, forKey: .maximumWeight) - try container.encodeIfPresent(alleyPriority, forKey: .alleyPriority) - try container.encodeIfPresent(walkwayPriority, forKey: .walkwayPriority) - try container.encodeIfPresent(speed, forKey: .speed) - - let formatter = DateFormatter.ISO8601DirectionsFormatter() - if let arriveBy { - try container.encode(formatter.string(from: arriveBy), forKey: .arriveBy) - } - if let departAt { - try container.encode(formatter.string(from: departAt), forKey: .departAt) - } - - if includesTollPrices { - try container.encode(includesTollPrices, forKey: .includesTollPrices) - } - } - - public required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.allowsUTurnAtWaypoint = try container.decode(Bool.self, forKey: .allowsUTurnAtWaypoint) - - self.includesAlternativeRoutes = try container.decode(Bool.self, forKey: .includesAlternativeRoutes) - - self.includesExitRoundaboutManeuver = try container.decode(Bool.self, forKey: .includesExitRoundaboutManeuver) - - self.roadClassesToAvoid = try container.decode(RoadClasses.self, forKey: .roadClassesToAvoid) - - self.roadClassesToAllow = try container.decode(RoadClasses.self, forKey: .roadClassesToAllow) - - self.refreshingEnabled = try container.decode(Bool.self, forKey: .refreshingEnabled) - - self._initialManeuverAvoidanceRadius = try container.decodeIfPresent( - LocationDistance.self, - forKey: .initialManeuverAvoidanceRadius - ) - - if let maximumHeightValue = try container.decodeIfPresent(Double.self, forKey: .maximumHeight) { - self.maximumHeight = Measurement(value: maximumHeightValue, unit: .meters) - } - - if let maximumWidthValue = try container.decodeIfPresent(Double.self, forKey: .maximumWidth) { - self.maximumWidth = Measurement(value: maximumWidthValue, unit: .meters) - } - if let maximumWeightValue = try container.decodeIfPresent(Double.self, forKey: .maximumWeight) { - self.maximumWeight = Measurement(value: maximumWeightValue, unit: .metricTons) - } - - self.alleyPriority = try container.decodeIfPresent(DirectionsPriority.self, forKey: .alleyPriority) - - self.walkwayPriority = try container.decodeIfPresent(DirectionsPriority.self, forKey: .walkwayPriority) - - self.speed = try container.decodeIfPresent(LocationSpeed.self, forKey: .speed) - - let formatter = DateFormatter.ISO8601DirectionsFormatter() - if let dateString = try container.decodeIfPresent(String.self, forKey: .departAt) { - self.departAt = formatter.date(from: dateString) - } - - if let dateString = try container.decodeIfPresent(String.self, forKey: .arriveBy) { - self.arriveBy = formatter.date(from: dateString) - } - - self.includesTollPrices = try container.decodeIfPresent(Bool.self, forKey: .includesTollPrices) ?? false - - try super.init(from: decoder) - } - - /// Initializes an equivalent route options object from a match options object. Desirable for building a navigation - /// experience from map matching. - /// - /// - Parameter matchOptions: The ``MatchOptions`` that is being used to convert to a ``RouteOptions`` object. - public convenience init(matchOptions: MatchOptions) { - self.init(waypoints: matchOptions.waypoints, profileIdentifier: matchOptions.profileIdentifier) - self.includesSteps = matchOptions.includesSteps - self.shapeFormat = matchOptions.shapeFormat - self.attributeOptions = matchOptions.attributeOptions - self.routeShapeResolution = matchOptions.routeShapeResolution - self.locale = matchOptions.locale - self.includesSpokenInstructions = matchOptions.includesSpokenInstructions - self.includesVisualInstructions = matchOptions.includesVisualInstructions - } - - override var abridgedPath: String { - return "directions/v5/\(profileIdentifier.rawValue)" - } - - // MARK: Influencing the Path of the Route - - /// A Boolean value that indicates whether a returned route may require a point U-turn at an intermediate waypoint. - /// - /// If the value of this property is `true`, a returned route may require an immediate U-turn at an intermediate - /// waypoint. At an intermediate waypoint, if the value of this property is `false`, each returned route may - /// continue straight ahead or turn to either side but may not U-turn. This property has no effect if only two - /// waypoints are specified. - /// - /// Set this property to `true` if you expect the user to traverse each leg of the trip separately. For example, it - /// would be quite easy for the user to effectively “U-turn” at a waypoint if the user first parks the car and - /// patronizes a restaurant there before embarking on the next leg of the trip. Set this property to `false` if you - /// expect the user to proceed to the next waypoint immediately upon arrival. For example, if the user only needs to - /// drop off a passenger or package at the waypoint before continuing, it would be inconvenient to perform a U-turn - /// at that location. - /// - /// The default value of this property is `false` when the profile identifier is ``ProfileIdentifier/automobile`` or - /// ``ProfileIdentifier/automobileAvoidingTraffic`` and `true` otherwise. - open var allowsUTurnAtWaypoint: Bool - - /// The route classes that the calculated routes will avoid. - /// - /// Currently, you can only specify a single road class to avoid. - open var roadClassesToAvoid: RoadClasses = [] - - /// The route classes that the calculated routes will allow. - /// - /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/automobile`` or - /// ``ProfileIdentifier/automobileAvoidingTraffic`` - open var roadClassesToAllow: RoadClasses = [] - - /// The number that influences whether the route should prefer or avoid alleys or narrow service roads between - /// buildings. - /// If this property isn't explicitly set, the Directions API will choose the most reasonable value. - /// - /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/automobile`` or - /// ``ProfileIdentifier/walking``. - /// - /// The value of this property must be at least ``DirectionsPriority/low`` and at most ``DirectionsPriority/high``. - /// ``DirectionsPriority/medium`` neither prefers nor avoids alleys, while a negative value between - /// ``DirectionsPriority/low`` and ``DirectionsPriority/medium`` avoids alleys, and a positive value between - /// ``DirectionsPriority/medium`` and ``DirectionsPriority/high`` prefers alleys. A value of 0.9 is suitable for - /// pedestrians who are comfortable with walking down alleys. - open var alleyPriority: DirectionsPriority? - - /// The number that influences whether the route should prefer or avoid roads or paths that are set aside for - /// pedestrian-only use (walkways or footpaths). - /// If this property isn't explicitly set, the Directions API will choose the most reasonable value. - /// - /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/walking``. You can - /// adjust this property to avoid [sidewalks and crosswalks that are mapped as separate - /// footpaths](https://wiki.openstreetmap.org/wiki/Sidewalks#Sidewalk_as_separate_way), which may be more granular - /// than needed for some forms of pedestrian navigation. - /// - /// The value of this property must be at least ``DirectionsPriority/low`` and at most ``DirectionsPriority/high``. - /// ``DirectionsPriority/medium`` neither prefers nor avoids walkways, while a negative value between - /// ``DirectionsPriority/low`` and ``DirectionsPriority/medium`` avoids walkways, and a positive value between - /// ``DirectionsPriority/medium`` and ``DirectionsPriority/high`` prefers walkways. A value of −0.1 results in less - /// verbose routes in cities where sidewalks and crosswalks are generally mapped as separate footpaths. - open var walkwayPriority: DirectionsPriority? - - /// The expected uniform travel speed measured in meters per second. - /// If this property isn't explicitly set, the Directions API will choose the most reasonable value. - /// - /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/walking``. You can - /// adjust this property to account for running or for faster or slower gaits. When the profile identifier is set to - /// another profile identifier, such as ``ProfileIdentifier/automobile`, this property is ignored in favor of the - /// expected travel speed on each road along the route. This property may be supported by other routing profiles in - /// the future. - /// - /// The value of this property must be at least `CLLocationSpeed.minimumWalking` and at most - /// `CLLocationSpeed.maximumWalking`. `CLLocationSpeed.normalWalking` corresponds to a typical preferred walking - /// speed. - open var speed: LocationSpeed? - - /// The desired arrival time, ignoring seconds precision, in the local time at the route destination. - /// - /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/automobile``. - open var arriveBy: Date? - - /// The desired departure time, ignoring seconds precision, in the local time at the route origin - /// - /// This property has no effect unless the profile identifier is set to ``ProfileIdentifier/automobile`` or - /// ``ProfileIdentifier/automobileAvoidingTraffic``. - open var departAt: Date? - - // MARK: Specifying the Response Format - - /// A Boolean value indicating whether alternative routes should be included in the response. - /// - /// If the value of this property is `false`, the server only calculates a single route that visits each of the - /// waypoints. If the value of this property is `true`, the server attempts to find additional reasonable routes - /// that visit the waypoints. Regardless, multiple routes are only returned if it is possible to visit the waypoints - /// by a different route without significantly increasing the distance or travel time. The alternative routes may - /// partially overlap with the preferred route, especially if intermediate waypoints are specified. - /// - /// Alternative routes may take longer to calculate and make the response significantly larger, so only request - /// alternative routes if you intend to display them to the user or let the user choose them over the preferred - /// route. For example, do not request alternative routes if you only want to know the distance or estimated travel - /// time to a destination. - /// - /// The default value of this property is `false`. - open var includesAlternativeRoutes = false - - /// A Boolean value indicating whether the route includes a ``ManeuverType/exitRoundabout`` or - /// ``ManeuverType/exitRotary`` step when traversing a roundabout or rotary, respectively. - /// - /// If this option is set to `true`, a route that traverses a roundabout includes both a - /// ``ManeuverType/takeRoundabout`` step and a ``ManeuverType/exitRoundabout`` step; likewise, a route that - /// traverses a large, named roundabout includes both a ``ManeuverType/takeRotary`` step and a - /// ``ManeuverType/exitRotary`` step. Otherwise, it only includes a ``ManeuverType/takeRoundabout`` or - /// ``ManeuverType/takeRotary`` step. This option is set to `false` by default. - open var includesExitRoundaboutManeuver = false - - /// A Boolean value indicating whether `Directions` can refresh time-dependent properties of the ``RouteLeg``s of - /// the resulting ``Route``s. - /// - /// To refresh the ``RouteLeg/expectedSegmentTravelTimes``, ``RouteLeg/segmentSpeeds``, and - /// ``RouteLeg/segmentCongestionLevels`` properties, use the - /// `Directions.refreshRoute(responseIdentifier:routeIndex:fromLegAtIndex:completionHandler:)` method. This property - /// is ignored unless ``DirectionsOptions/profileIdentifier`` is ``ProfileIdentifier/automobileAvoidingTraffic``. - /// This option is set - /// to `false` by default. - open var refreshingEnabled = false - - /// The maximum vehicle height. - /// - /// If this parameter is provided, `Directions` will compute a route that includes only roads with a height limit - /// greater than or equal to the max vehicle height or no height limit. - /// - /// This property is supported by ``ProfileIdentifier/automobile`` and - /// ``ProfileIdentifier/automobileAvoidingTraffic`` profiles. - /// The value must be between 0 and 10 when converted to meters. - open var maximumHeight: Measurement? - - /// The maximum vehicle width. - /// - /// If this parameter is provided, `Directions` will compute a route that includes only roads with a width limit - /// greater than or equal to the max vehicle width or no width limit. - /// This property is supported by ``ProfileIdentifier/automobile`` and - /// ``ProfileIdentifier/automobileAvoidingTraffic`` profiles. - /// The value must be between 0 and 10 when converted to meters. - open var maximumWidth: Measurement? - - /// The maximum vehicle weight. - /// - /// If this parameter is provided, the `Directions` will compute a route that includes only roads with a weight - /// limit greater than or equal to the max vehicle weight. - /// This property is supported by ``ProfileIdentifier/automobile`` and - /// ``ProfileIdentifier/automobileAvoidingTraffic`` profiles. - /// The value must be between 0 and 100 metric tons. If unspecified, 2.5 metric tons is assumed. - open var maximumWeight: Measurement? - /// A radius around the starting point in which the API will avoid returning any significant maneuvers. - /// - /// Use this option when the vehicle is traveling at a significant speed to avoid dangerous maneuvers when - /// re-routing. If a route is not found using the specified value, it will be ignored. Note that if a large radius - /// is used, the API may ignore an important turn and return a long straight path before the first maneuver. - /// - /// This value is clamped to `LocationDistance.minimumManeuverIgnoringRadius` and - /// `LocationDistance.maximumManeuverIgnoringRadius`. - open var initialManeuverAvoidanceRadius: LocationDistance? { - get { - _initialManeuverAvoidanceRadius - } - set { - _initialManeuverAvoidanceRadius = newValue.map { - min( - LocationDistance.maximumManeuverIgnoringRadius, - max( - LocationDistance.minimumManeuverIgnoringRadius, - $0 - ) - ) - } - } - } - - private var _initialManeuverAvoidanceRadius: LocationDistance? - - /// Toggle whether to return calculated toll cost for the route, if data is available. - /// - /// Toll prices are populeted in resulting route's ``Route/tollPrices``. - /// Default value is `false`. - open var includesTollPrices = false - - // MARK: Getting the Request URL - - override open var urlQueryItems: [URLQueryItem] { - var params: [URLQueryItem] = [ - URLQueryItem( - name: CodingKeys.includesAlternativeRoutes.stringValue, - value: includesAlternativeRoutes.queryString - ), - URLQueryItem( - name: CodingKeys.allowsUTurnAtWaypoint.stringValue, - value: (!allowsUTurnAtWaypoint).queryString - ), - ] - - if includesExitRoundaboutManeuver { - params.append(URLQueryItem( - name: CodingKeys.includesExitRoundaboutManeuver.stringValue, - value: includesExitRoundaboutManeuver.queryString - )) - } - if let alleyPriority = alleyPriority?.rawValue { - params.append(URLQueryItem(name: CodingKeys.alleyPriority.stringValue, value: String(alleyPriority))) - } - - if let walkwayPriority = walkwayPriority?.rawValue { - params.append(URLQueryItem(name: CodingKeys.walkwayPriority.stringValue, value: String(walkwayPriority))) - } - - if let speed { - params.append(URLQueryItem(name: CodingKeys.speed.stringValue, value: String(speed))) - } - - if !roadClassesToAvoid.isEmpty { - let roadClasses = roadClassesToAvoid.description - params.append(URLQueryItem(name: CodingKeys.roadClassesToAvoid.stringValue, value: roadClasses)) - } - - if !roadClassesToAllow.isEmpty { - let parameterValue = roadClassesToAllow.description - params.append(URLQueryItem(name: CodingKeys.roadClassesToAllow.stringValue, value: parameterValue)) - } - - if refreshingEnabled, profileIdentifier == .automobileAvoidingTraffic { - params.append(URLQueryItem( - name: CodingKeys.refreshingEnabled.stringValue, - value: refreshingEnabled.queryString - )) - } - - if waypoints.first(where: { $0.targetCoordinate != nil }) != nil { - let targetCoordinates = waypoints.filter(\.separatesLegs) - .map { $0.targetCoordinate?.requestDescription ?? "" }.joined(separator: ";") - params.append(URLQueryItem(name: CodingKeys.waypointTargets.stringValue, value: targetCoordinates)) - } - - if waypoints.contains(where: { $0.layer != nil }) { - let layers = waypoints.map { $0.layer?.description ?? "" }.joined(separator: ";") - params.append(URLQueryItem(name: CodingKeys.layers.stringValue, value: layers)) - } - - if let initialManeuverAvoidanceRadius { - params.append(URLQueryItem( - name: CodingKeys.initialManeuverAvoidanceRadius.stringValue, - value: String(initialManeuverAvoidanceRadius) - )) - } - - if let maximumHeight { - let heightInMeters = maximumHeight.converted(to: .meters).value - params.append(URLQueryItem(name: CodingKeys.maximumHeight.stringValue, value: String(heightInMeters))) - } - - if let maximumWidth { - let widthInMeters = maximumWidth.converted(to: .meters).value - params.append(URLQueryItem(name: CodingKeys.maximumWidth.stringValue, value: String(widthInMeters))) - } - - if let maximumWeight { - let weightInTonnes = maximumWeight.converted(to: .metricTons).value - params.append(URLQueryItem(name: CodingKeys.maximumWeight.stringValue, value: String(weightInTonnes))) - } - - if [ProfileIdentifier.automobile, .automobileAvoidingTraffic].contains(profileIdentifier) { - let formatter = DateFormatter.ISO8601DirectionsFormatter() - - if let departAt { - params.append(URLQueryItem( - name: CodingKeys.departAt.stringValue, - value: String(formatter.string(from: departAt)) - )) - } - - if profileIdentifier == .automobile, - let arriveBy - { - params.append(URLQueryItem( - name: CodingKeys.arriveBy.stringValue, - value: String(formatter.string(from: arriveBy)) - )) - } - } - - if includesTollPrices { - params.append(URLQueryItem( - name: CodingKeys.includesTollPrices.stringValue, - value: includesTollPrices.queryString - )) - } - - return params + super.urlQueryItems - } -} - -@available(*, unavailable) -extension RouteOptions: @unchecked Sendable {} - -extension Bool { - var queryString: String { - return self ? "true" : "false" - } -} - -extension LocationSpeed { - /// Pedestrians are assumed to walk at an average rate of 1.42 meters per second (5.11 kilometers per hour or 3.18 - /// miles per hour), corresponding to a typical preferred walking speed. - static let normalWalking: LocationSpeed = 1.42 - - /// Pedestrians are assumed to walk no slower than 0.14 meters per second (0.50 kilometers per hour or 0.31 miles - /// per hour) on average. - static let minimumWalking: LocationSpeed = 0.14 - - /// Pedestrians are assumed to walk no faster than 6.94 meters per second (25.0 kilometers per hour or 15.5 miles - /// per hour) on average. - static let maximumWalking: LocationSpeed = 6.94 -} - -extension LocationDistance { - /// Minimum positive value to ignore maneuvers around origin point during routing. - static let minimumManeuverIgnoringRadius: LocationDistance = 1 - - /// Maximum value to ignore maneuvers around origin point during routing. - static let maximumManeuverIgnoringRadius: LocationDistance = 1000 -} - -extension DateFormatter { - /// Special ISO8601 date converter for `depart_at` and `arrive_by` parameters, as Directions API explicitly require - /// no seconds bit. - fileprivate static func ISO8601DirectionsFormatter() -> DateFormatter { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm" - formatter.timeZone = TimeZone(secondsFromGMT: 0) - return formatter - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteRefreshResponse.swift b/ios/Classes/Navigation/MapboxDirections/RouteRefreshResponse.swift deleted file mode 100644 index 5d17cb383..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RouteRefreshResponse.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif -import Turf - -/// A Directions Refresh API response. -public struct RouteRefreshResponse: ForeignMemberContainer, Equatable { - public var foreignMembers: JSONObject = [:] - - /// The raw HTTP response from the Directions Refresh API. - public let httpResponse: HTTPURLResponse? - - /// The response identifier used to request the refreshed route. - public let identifier: String - - /// The route index used to request the refreshed route. - public var routeIndex: Int - - public var startLegIndex: Int - - /// A skeleton route that contains only the time-sensitive information that has been updated. - public var route: RefreshedRoute - - /// The credentials used to make the request. - public let credentials: Credentials - - /// The time when this ``RouteRefreshResponse`` object was created, which is immediately upon recieving the raw URL - /// response. - /// - /// If you manually start fetching a task returned by - /// `Directions.urlRequest(forRefreshing:routeIndex:currentLegIndex:)`, this property is set to `nil`; use the - /// `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be set to `nil` if - /// you create this result from a JSON object or encoded object. - /// - /// This property does not persist after encoding and decoding. - public var created = Date() -} - -extension RouteRefreshResponse: Codable { - enum CodingKeys: String, CodingKey { - case identifier = "uuid" - case route - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse - - guard let credentials = decoder.userInfo[.credentials] as? Credentials else { - throw DirectionsCodingError.missingCredentials - } - - self.credentials = credentials - - if let identifier = decoder.userInfo[.responseIdentifier] as? String { - self.identifier = identifier - } else { - throw DirectionsCodingError.missingOptions - } - - self.route = try container.decode(RefreshedRoute.self, forKey: .route) - - if let routeIndex = decoder.userInfo[.routeIndex] as? Int { - self.routeIndex = routeIndex - } else { - throw DirectionsCodingError.missingOptions - } - - if let startLegIndex = decoder.userInfo[.startLegIndex] as? Int { - self.startLegIndex = startLegIndex - } else { - throw DirectionsCodingError.missingOptions - } - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(identifier, forKey: .identifier) - - try container.encode(route, forKey: .route) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteRefreshSource.swift b/ios/Classes/Navigation/MapboxDirections/RouteRefreshSource.swift deleted file mode 100644 index 08e9ce0d1..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RouteRefreshSource.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation - -/// A skeletal route containing infromation to refresh ``Route`` object attributes. -public protocol RouteRefreshSource { - var refreshedLegs: [RouteLegRefreshSource] { get } -} - -/// A skeletal route leg containing infromation to refresh ``RouteLeg`` object attributes. -public protocol RouteLegRefreshSource { - var refreshedAttributes: RouteLeg.Attributes { get } - var refreshedIncidents: [Incident]? { get } - var refreshedClosures: [RouteLeg.Closure]? { get } -} - -extension RouteLegRefreshSource { - public var refreshedIncidents: [Incident]? { - return nil - } - - public var refreshedClosures: [RouteLeg.Closure]? { - return nil - } -} - -extension Route: RouteRefreshSource { - public var refreshedLegs: [RouteLegRefreshSource] { - legs - } -} - -extension RouteLeg: RouteLegRefreshSource { - public var refreshedAttributes: Attributes { - attributes - } - - public var refreshedIncidents: [Incident]? { - incidents - } - - public var refreshedClosures: [RouteLeg.Closure]? { - closures - } -} - -extension RefreshedRoute: RouteRefreshSource { - public var refreshedLegs: [RouteLegRefreshSource] { - legs - } -} - -extension RefreshedRouteLeg: RouteLegRefreshSource { - public var refreshedAttributes: RouteLeg.Attributes { - attributes - } - - public var refreshedIncidents: [Incident]? { - incidents - } - - public var refreshedClosures: [RouteLeg.Closure]? { - closures - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteResponse.swift b/ios/Classes/Navigation/MapboxDirections/RouteResponse.swift deleted file mode 100644 index 212a41052..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RouteResponse.swift +++ /dev/null @@ -1,393 +0,0 @@ -import Foundation -import Turf -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -public enum ResponseOptions: Sendable { - case route(RouteOptions) - case match(MatchOptions) -} - -@available(*, unavailable) -extension ResponseOptions: @unchecked Sendable {} - -/// A ``RouteResponse`` object is a structure that corresponds to a directions response returned by the Mapbox -/// Directions API. -public struct RouteResponse: ForeignMemberContainer { - public var foreignMembers: JSONObject = [:] - - /// The raw HTTP response from the Directions API. - public let httpResponse: HTTPURLResponse? - - /// The unique identifier that the Mapbox Directions API has assigned to this response. - public let identifier: String? - - /// An array of ``Route`` objects sorted from most recommended to least recommended. A route may be highly - /// recommended - /// based on characteristics such as expected travel time or distance. - /// This property contains a maximum of two ``Route``s. - public var routes: [Route]? { - didSet { - updateRoadClassExclusionViolations() - } - } - - /// An array of ``Waypoint`` objects in the order of the input coordinates. Each ``Waypoint`` is an input coordinate - /// snapped to the road and path network. - /// - /// This property omits the waypoint corresponding to any waypoint in ``DirectionsOptions/waypoints`` that has - /// ``Waypoint/separatesLegs`` set to `true`. - public let waypoints: [Waypoint]? - - /// The criteria for the directions response. - public let options: ResponseOptions - - /// The credentials used to make the request. - public let credentials: Credentials - - /// The time when this ``RouteResponse`` object was created, which is immediately upon recieving the raw URL - /// response. - /// - /// If you manually start fetching a task returned by `Directions.url(forCalculating:)`, this property is set to - /// `nil`; use the `URLSessionTaskTransactionMetrics.responseEndDate` property instead. This property may also be - /// set to `nil` if you create this result from a JSON object or encoded object. - /// - /// This property does not persist after encoding and decoding. - public var created: Date = .init() - - /// A time period during which the routes from this ``RouteResponse`` are eligable for refreshing. - /// - /// `nil` value indicates that route refreshing is not available for related routes. - public let refreshTTL: TimeInterval? - - /// A deadline after which the routes from this ``RouteResponse`` are eligable for refreshing. - /// - /// `nil` value indicates that route refreshing is not available for related routes. - public var refreshInvalidationDate: Date? { - refreshTTL.map { created.addingTimeInterval($0) } - } - - /// Managed array of ``RoadClasses`` restrictions specified to ``RouteOptions/roadClassesToAvoid`` which were - /// violated - /// during route calculation. - /// - /// Routing engine may still utilize ``RoadClasses`` meant to be avoided in cases when routing is impossible - /// otherwise. - /// - /// Violations are ordered by routes from the ``routes`` array, then by a leg, step, and intersection, where - /// ``RoadClasses`` restrictions were ignored. `nil` and empty return arrays correspond to `nil` and empty - /// ``routes`` - /// array respectively. - public private(set) var roadClassExclusionViolations: [RoadClassExclusionViolation]? -} - -extension RouteResponse: Codable { - enum CodingKeys: String, CodingKey { - case message - case error - case identifier = "uuid" - case routes - case waypoints - case refreshTTL = "refresh_ttl" - } - - public init( - httpResponse: HTTPURLResponse?, - identifier: String? = nil, - routes: [Route]? = nil, - waypoints: [Waypoint]? = nil, - options: ResponseOptions, - credentials: Credentials, - refreshTTL: TimeInterval? = nil - ) { - self.httpResponse = httpResponse - self.identifier = identifier - self.options = options - self.routes = routes - self.waypoints = waypoints - self.credentials = credentials - self.refreshTTL = refreshTTL - - updateRoadClassExclusionViolations() - } - - public init(matching response: MapMatchingResponse, options: MatchOptions, credentials: Credentials) throws { - let decoder = JSONDecoder() - let encoder = JSONEncoder() - - decoder.userInfo[.options] = options - decoder.userInfo[.credentials] = credentials - encoder.userInfo[.options] = options - encoder.userInfo[.credentials] = credentials - - var routes: [Route]? - - if let matches = response.matches { - let matchesData = try encoder.encode(matches) - routes = try decoder.decode([Route].self, from: matchesData) - } - - var waypoints: [Waypoint]? - - if let tracepoints = response.tracepoints { - let filtered = tracepoints.compactMap { $0 } - let tracepointsData = try encoder.encode(filtered) - waypoints = try decoder.decode([Waypoint].self, from: tracepointsData) - } - - self.init( - httpResponse: response.httpResponse, - identifier: nil, - routes: routes, - waypoints: waypoints, - options: .match(options), - credentials: credentials - ) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse - - guard let credentials = decoder.userInfo[.credentials] as? Credentials else { - throw DirectionsCodingError.missingCredentials - } - - self.credentials = credentials - - if let options = decoder.userInfo[.options] as? RouteOptions { - self.options = .route(options) - } else if let options = decoder.userInfo[.options] as? MatchOptions { - self.options = .match(options) - } else { - throw DirectionsCodingError.missingOptions - } - - self.identifier = try container.decodeIfPresent(String.self, forKey: .identifier) - - // Decode waypoints from the response and update their names according to the waypoints from - // DirectionsOptions.waypoints. - let decodedWaypoints = try container.decodeIfPresent([Waypoint?].self, forKey: .waypoints)?.compactMap { $0 } - var optionsWaypoints: [Waypoint] = [] - - switch options { - case .match(options: let matchOpts): - optionsWaypoints = matchOpts.waypoints - case .route(options: let routeOpts): - optionsWaypoints = routeOpts.waypoints - } - - if let decodedWaypoints { - // The response lists the same number of tracepoints as the waypoints in the request, whether or not a given - // waypoint is leg-separating. - var waypoints = zip(decodedWaypoints, optionsWaypoints).map { pair -> Waypoint in - let (decodedWaypoint, waypointInOptions) = pair - var waypoint = Waypoint( - coordinate: decodedWaypoint.coordinate, - coordinateAccuracy: waypointInOptions.coordinateAccuracy, - name: waypointInOptions.name?.nonEmptyString ?? decodedWaypoint.name - ) - waypoint.snappedDistance = decodedWaypoint.snappedDistance - waypoint.targetCoordinate = waypointInOptions.targetCoordinate - waypoint.heading = waypointInOptions.heading - waypoint.headingAccuracy = waypointInOptions.headingAccuracy - waypoint.separatesLegs = waypointInOptions.separatesLegs - waypoint.allowsArrivingOnOppositeSide = waypointInOptions.allowsArrivingOnOppositeSide - - waypoint.foreignMembers = decodedWaypoint.foreignMembers - - return waypoint - } - - if waypoints.startIndex < waypoints.endIndex { - waypoints[waypoints.startIndex].separatesLegs = true - } - let lastIndex = waypoints.endIndex - 1 - if waypoints.indices.contains(lastIndex) { - waypoints[lastIndex].separatesLegs = true - } - - self.waypoints = waypoints - } else { - self.waypoints = decodedWaypoints - } - - if var routes = try container.decodeIfPresent([Route].self, forKey: .routes) { - // Postprocess each route. - for routeIndex in routes.indices { - // Imbue each route’s legs with the waypoints refined above. - if let waypoints { - routes[routeIndex].legSeparators = waypoints.filter(\.separatesLegs) - } - } - self.routes = routes - } else { - self.routes = nil - } - - self.refreshTTL = try container.decodeIfPresent(TimeInterval.self, forKey: .refreshTTL) - - updateRoadClassExclusionViolations() - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(identifier, forKey: .identifier) - try container.encodeIfPresent(routes, forKey: .routes) - try container.encodeIfPresent(waypoints, forKey: .waypoints) - try container.encodeIfPresent(refreshTTL, forKey: .refreshTTL) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } -} - -extension RouteResponse { - mutating func updateRoadClassExclusionViolations() { - guard case .route(let routeOptions) = options else { - roadClassExclusionViolations = nil - return - } - - guard let routes else { - roadClassExclusionViolations = nil - return - } - - let avoidedClasses = routeOptions.roadClassesToAvoid - - guard !avoidedClasses.isEmpty else { - roadClassExclusionViolations = nil - return - } - - var violations = [RoadClassExclusionViolation]() - - for (routeIndex, route) in routes.enumerated() { - for (legIndex, leg) in route.legs.enumerated() { - for (stepIndex, step) in leg.steps.enumerated() { - for (intersectionIndex, intersection) in (step.intersections ?? []).enumerated() { - if let outletRoadClasses = intersection.outletRoadClasses, - !avoidedClasses.isDisjoint(with: outletRoadClasses) - { - violations.append(RoadClassExclusionViolation( - roadClasses: avoidedClasses.intersection(outletRoadClasses), - routeIndex: routeIndex, - legIndex: legIndex, - stepIndex: stepIndex, - intersectionIndex: intersectionIndex - )) - } - } - } - } - } - roadClassExclusionViolations = violations - } - - /// Filters ``roadClassExclusionViolations`` lazily to search for specific leg and step. - /// - /// - parameter routeIndex: Index of a route inside current ``RouteResponse`` to search in. - /// - parameter legIndex: Index of a leg inside related ``Route``to search in. - /// - returns: Lazy filtered array of ``RoadClassExclusionViolation`` under given indicies. - /// - /// Passing `nil` as `legIndex` will result in searching for all legs. - public func exclusionViolations( - routeIndex: Int, - legIndex: Int? = nil - ) -> LazyFilterSequence<[RoadClassExclusionViolation]> { - return filteredViolations( - routeIndex: routeIndex, - legIndex: legIndex, - stepIndex: nil, - intersectionIndex: nil - ) - } - - /// Filters ``roadClassExclusionViolations`` lazily to search for specific leg and step. - /// - /// - parameter routeIndex: Index of a route inside current ``RouteResponse`` to search in. - /// - parameter legIndex: Index of a leg inside related ``Route``to search in. - /// - parameter stepIndex: Index of a step inside given ``Route``'s leg. - /// - returns: Lazy filtered array of ``RoadClassExclusionViolation`` under given indicies. - /// - /// Passing `nil` as `stepIndex` will result in searching for all steps. - public func exclusionViolations( - routeIndex: Int, - legIndex: Int, - stepIndex: Int? = nil - ) -> LazyFilterSequence<[RoadClassExclusionViolation]> { - return filteredViolations( - routeIndex: routeIndex, - legIndex: legIndex, - stepIndex: stepIndex, - intersectionIndex: nil - ) - } - - /// Filters ``roadClassExclusionViolations`` lazily to search for specific leg, step and intersection. - /// - /// - parameter routeIndex: Index of a route inside current ``RouteResponse`` to search in. - /// - parameter legIndex: Index of a leg inside related ``Route``to search in. - /// - parameter stepIndex: Index of a step inside given ``Route``'s leg. - /// - parameter intersectionIndex: Index of an intersection inside given ``Route``'s leg and step. - /// - returns: Lazy filtered array of ``RoadClassExclusionViolation`` under given indicies. - /// - /// Passing `nil` as `intersectionIndex` will result in searching for all intersections of given step. - public func exclusionViolations( - routeIndex: Int, - legIndex: Int, - stepIndex: Int, - intersectionIndex: Int? - ) -> LazyFilterSequence<[RoadClassExclusionViolation]> { - return filteredViolations( - routeIndex: routeIndex, - legIndex: legIndex, - stepIndex: stepIndex, - intersectionIndex: intersectionIndex - ) - } - - private func filteredViolations( - routeIndex: Int, - legIndex: Int? = nil, - stepIndex: Int? = nil, - intersectionIndex: Int? = nil - ) -> LazyFilterSequence<[RoadClassExclusionViolation]> { - assert( - !(stepIndex == nil && intersectionIndex != nil), - "It is forbidden to select `intersectionIndex` without specifying `stepIndex`." - ) - - guard let roadClassExclusionViolations else { - return LazyFilterSequence<[RoadClassExclusionViolation]>(_base: [], { _ in true }) - } - - var filtered = roadClassExclusionViolations.lazy.filter { - $0.routeIndex == routeIndex - } - - if let legIndex { - filtered = filtered.filter { - $0.legIndex == legIndex - } - } - - if let stepIndex { - filtered = filtered.filter { - $0.stepIndex == stepIndex - } - } - - if let intersectionIndex { - filtered = filtered.filter { - $0.intersectionIndex == intersectionIndex - } - } - - return filtered - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/RouteStep.swift b/ios/Classes/Navigation/MapboxDirections/RouteStep.swift deleted file mode 100644 index 7226521f0..000000000 --- a/ios/Classes/Navigation/MapboxDirections/RouteStep.swift +++ /dev/null @@ -1,1112 +0,0 @@ -import Foundation -import Turf -#if canImport(CoreLocation) -import CoreLocation -#endif - -/// A ``TransportType`` specifies the mode of transportation used for part of a route. -public enum TransportType: String, Codable, Equatable, Sendable { - /// Possible transport types when the `profileIdentifier` is ``ProfileIdentifier/automobile`` or - /// ``ProfileIdentifier/automobileAvoidingTraffic`` - - /// The route requires the user to drive or ride a car, truck, or motorcycle. - /// This is the usual transport type when the `profileIdentifier` is ``ProfileIdentifier/automobile`` or - /// ``ProfileIdentifier/automobileAvoidingTraffic``. - case automobile = "driving" // automobile - - /// The route requires the user to board a ferry. - /// - /// The user should verify that the ferry is in operation. For driving and cycling directions, the user should also - /// verify that their vehicle is permitted onboard the ferry. - case ferry // automobile, walking, cycling - - /// The route requires the user to cross a movable bridge. - /// - /// The user may need to wait for the movable bridge to become passable before continuing. - case movableBridge = "movable bridge" // automobile, cycling - - /// The route becomes impassable at this point. - /// - /// You should not encounter this transport type under normal circumstances. - case inaccessible = "unaccessible" // automobile, walking, cycling - - /// Possible transport types when the `profileIdentifier` is ``ProfileIdentifier/walking`` - - /// The route requires the user to walk. - /// - /// This is the usual transport type when the `profileIdentifier` is ``ProfileIdentifier/walking``. For cycling - /// directions, this value indicates that the user is expected to dismount. - case walking // walking, cycling - - /// Possible transport types when the `profileIdentifier` is ``ProfileIdentifier/cycling`` - - /// The route requires the user to ride a bicycle. - /// - /// This is the usual transport type when the `profileIdentifier` is ``ProfileIdentifier/cycling``. - case cycling // cycling - - /// The route requires the user to board a train. - /// - /// The user should consult the train’s timetable. For cycling directions, the user should also verify that bicycles - /// are permitted onboard the train. - case train // cycling - - /// Custom implementation of decoding is needed to circumvent issue reported in - /// https://github.com/mapbox/mapbox-directions-swift/issues/413 - public init(from decoder: Decoder) throws { - let valueContainer = try decoder.singleValueContainer() - let rawValue = try valueContainer.decode(String.self) - - if rawValue == "pushing bike" { - self = .walking - - return - } - - guard let value = TransportType(rawValue: rawValue) else { - throw DecodingError.dataCorruptedError( - in: valueContainer, - debugDescription: "Cannot initialize TransportType from invalid String value \(rawValue)" - ) - } - - self = value - } -} - -/// A ``ManeuverType`` specifies the type of maneuver required to complete the route step. You can pair a maneuver type -/// with a ``ManeuverDirection`` to choose an appropriate visual or voice prompt to present the user. -/// -/// To avoid a complex series of if-else-if statements or switch statements, use pattern matching with a single switch -/// statement on a tuple that consists of the maneuver type and maneuver direction. -public enum ManeuverType: String, Codable, Equatable, Sendable { - /// The step requires the user to depart from a waypoint. - /// - /// If the waypoint is some distance away from the nearest road, the maneuver direction indicates the direction the - /// user must turn upon reaching the road. - case depart - - /// The step requires the user to turn. - /// - /// The maneuver direction indicates the direction in which the user must turn relative to the current direction of - /// travel. The exit index indicates the number of intersections, large or small, from the previous maneuver up to - /// and including the intersection at which the user must turn. - case turn - - /// The step requires the user to continue after a turn. - case `continue` - - /// The step requires the user to continue on the current road as it changes names. - /// - /// The step’s name contains the road’s new name. To get the road’s old name, use the previous step’s name. - case passNameChange = "new name" - - /// The step requires the user to merge onto another road. - /// - /// The maneuver direction indicates the side from which the other road approaches the intersection relative to the - /// user. - case merge - - /// The step requires the user to take a entrance ramp (slip road) onto a highway. - case takeOnRamp = "on ramp" - - /// The step requires the user to take an exit ramp (slip road) off a highway. - /// - /// The maneuver direction indicates the side of the highway from which the user must exit. The exit index indicates - /// the number of highway exits from the previous maneuver up to and including the exit that the user must take. - case takeOffRamp = "off ramp" - - /// The step requires the user to choose a fork at a Y-shaped fork in the road. - /// - /// The maneuver direction indicates which fork to take. - case reachFork = "fork" - - /// The step requires the user to turn at either a T-shaped three-way intersection or a sharp bend in the road where - /// the road also changes names. - /// - /// This maneuver type is called out separately so that the user may be able to proceed more confidently, without - /// fear of having overshot the turn. If this distinction is unimportant to you, you may treat the maneuver as an - /// ordinary ``ManeuverType/turn``. - case reachEnd = "end of road" - - /// The step requires the user to get into a specific lane in order to continue along the current road. - /// - /// The maneuver direction is set to ``ManeuverDirection/straightAhead``. Each of the first intersection’s usable - /// approach lanes also has an indication of ``LaneIndication/straightAhead``. A maneuver in a different direction - /// would instead have a maneuver type of ``ManeuverType/turn``. - /// - /// This maneuver type is called out separately so that the application can present the user with lane guidance - /// based on the first element in the ``RouteStep/intersections`` property. If lane guidance is unimportant to you, - /// you may - /// treat the maneuver as an ordinary ``ManeuverType/continue`` or ignore it. - case useLane = "use lane" - - /// The step requires the user to enter and traverse a roundabout (traffic circle or rotary). - /// - /// The step has no name, but the exit name is the name of the road to take to exit the roundabout. The exit index - /// indicates the number of roundabout exits up to and including the exit to take. - /// - /// If ``RouteOptions/includesExitRoundaboutManeuver`` is set to `true`, this step is followed by an - /// ``ManeuverType/exitRoundabout`` maneuver. Otherwise, this step represents the entire roundabout maneuver, from - /// the entrance to the exit. - case takeRoundabout = "roundabout" - - /// The step requires the user to enter and traverse a large, named roundabout (traffic circle or rotary). - /// - /// The step’s name is the name of the roundabout. The exit name is the name of the road to take to exit the - /// roundabout. The exit index indicates the number of rotary exits up to and including the exit that the user must - /// take. - /// - /// If ``RouteOptions/includesExitRoundaboutManeuver`` is set to `true`, this step is followed by an - /// ``ManeuverType/exitRotary`` maneuver. Otherwise, this step represents the entire roundabout maneuver, from the - /// entrance to the exit. - case takeRotary = "rotary" - - /// The step requires the user to enter and exit a roundabout (traffic circle or rotary) that is compact enough to - /// constitute a single intersection. - /// - /// The step’s name is the name of the road to take after exiting the roundabout. This maneuver type is called out - /// separately because the user may perceive the roundabout as an ordinary intersection with an island in the - /// middle. If this distinction is unimportant to you, you may treat the maneuver as either an ordinary - /// ``ManeuverType/turn`` or as a ``ManeuverType/takeRoundabout``. - case turnAtRoundabout = "roundabout turn" - - /// The step requires the user to exit a roundabout (traffic circle or rotary). - /// - /// This maneuver type follows a ``ManeuverType/takeRoundabout`` maneuver. It is only used when - /// ``RouteOptions/includesExitRoundaboutManeuver`` is set to true. - case exitRoundabout = "exit roundabout" - - /// The step requires the user to exit a large, named roundabout (traffic circle or rotary). - /// - /// This maneuver type follows a ``ManeuverType/takeRotary`` maneuver. It is only used when - /// ``RouteOptions/includesExitRoundaboutManeuver`` is set to true. - case exitRotary = "exit rotary" - - /// The step requires the user to respond to a change in travel conditions. - /// - /// This maneuver type may occur for example when driving directions require the user to board a ferry, or when - /// cycling directions require the user to dismount. The step’s transport type and instructions contains important - /// contextual details that should be presented to the user at the maneuver location. - /// - /// Similar changes can occur simultaneously with other maneuvers, such as when the road changes its name at the - /// site of a movable bridge. In such cases, ``heedWarning`` is suppressed in favor of another maneuver type. - case heedWarning = "notification" - - /// The step requires the user to arrive at a waypoint. - /// - /// The distance and expected travel time for this step are set to zero, indicating that the route or route leg is - /// complete. The maneuver direction indicates the side of the road on which the waypoint can be found (or whether - /// it is straight ahead). - case arrive - - // Unrecognized maneuver types are interpreted as turns. - // http://project-osrm.org/docs/v5.5.1/api/#stepmaneuver-object - static let `default` = ManeuverType.turn -} - -/// A ``ManeuverDirection`` clarifies a ``ManeuverType`` with directional information. The exact meaning of the maneuver -/// direction for a given step depends on the step’s maneuver type; see the ``ManeuverType`` documentation for details. -public enum ManeuverDirection: String, Codable, Equatable, Sendable { - /// The maneuver requires a sharp turn to the right. - case sharpRight = "sharp right" - - /// The maneuver requires a turn to the right, a merge to the right, or an exit on the right, or the destination is - /// on the right. - case right - - /// The maneuver requires a slight turn to the right. - case slightRight = "slight right" - - /// The maneuver requires no notable change in direction, or the destination is straight ahead. - case straightAhead = "straight" - - /// The maneuver requires a slight turn to the left. - case slightLeft = "slight left" - - /// The maneuver requires a turn to the left, a merge to the left, or an exit on the left, or the destination is on - /// the right. - case left - - /// The maneuver requires a sharp turn to the left. - case sharpLeft = "sharp left" - - /// The maneuver requires a U-turn when possible. - /// - /// Use the difference between the step’s initial and final headings to distinguish between a U-turn to the left - /// (typical in countries that drive on the right) and a U-turn on the right (typical in countries that drive on the - /// left). If the difference in headings is greater than 180 degrees, the maneuver requires a U-turn to the left. If - /// the difference in headings is less than 180 degrees, the maneuver requires a U-turn to the right. - case uTurn = "uturn" - - case undefined - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let rawValue = try? container.decode(String.self) { - self = ManeuverDirection(rawValue: rawValue) ?? .undefined - } else { - self = .undefined - } - } -} - -/// A road sign design standard. -/// -/// A sign standard can affect how a user interface should display information related to the road. For example, a speed -/// limit from the ``RouteLeg/segmentMaximumSpeedLimits`` property may appear in a different-looking view depending on -/// the ``RouteStep/speedLimitSign` property. -public enum SignStandard: String, Codable, Equatable, Sendable { - /// The [Manual on Uniform Traffic Control - /// Devices](https://en.wikipedia.org/wiki/Manual_on_Uniform_Traffic_Control_Devices). - /// - /// This standard has been adopted by the United States and Canada, and several other countries have adopted parts - /// of the standard as well. - case mutcd - - /// The [Vienna Convention on Road Signs and - /// Signals](https://en.wikipedia.org/wiki/Vienna_Convention_on_Road_Signs_and_Signals). - /// - /// This standard is prevalent in Europe and parts of Asia and Latin America. Countries in southern Africa and - /// Central America have adopted similar regional standards. - case viennaConvention = "vienna" -} - -extension String { - func tagValues(separatedBy separator: String) -> [String] { - return components(separatedBy: separator).map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } - } -} - -extension [String] { - func tagValues(joinedBy separator: String) -> String { - return joined(separator: "\(separator) ") - } -} - -/// Encapsulates all the information about a road. -struct Road: Equatable, Sendable { - let names: [String]? - let codes: [String]? - let exitCodes: [String]? - let destinations: [String]? - let destinationCodes: [String]? - let rotaryNames: [String]? - - init( - names: [String]?, - codes: [String]?, - exitCodes: [String]?, - destinations: [String]?, - destinationCodes: [String]?, - rotaryNames: [String]? - ) { - self.names = names - self.codes = codes - self.exitCodes = exitCodes - self.destinations = destinations - self.destinationCodes = destinationCodes - self.rotaryNames = rotaryNames - } - - init(name: String, ref: String?, exits: String?, destination: String?, rotaryName: String?) { - if !name.isEmpty, let ref { - // Directions API v5 profiles powered by Valhalla no longer include the ref in the name. However, the - // `mapbox/cycling` profile, which is powered by OSRM, still includes the ref. - let parenthetical = "(\(ref))" - if name == ref { - self.names = nil - } else { - self.names = name.replacingOccurrences(of: parenthetical, with: "").tagValues(separatedBy: ";") - } - } else { - self.names = name.isEmpty ? nil : name.tagValues(separatedBy: ";") - } - - // Mapbox Directions API v5 combines the destination’s ref and name. - if let destination, destination.contains(": ") { - let destinationComponents = destination.components(separatedBy: ": ") - self.destinationCodes = destinationComponents.first?.tagValues(separatedBy: ",") - self.destinations = destinationComponents.dropFirst().joined(separator: ": ").tagValues(separatedBy: ",") - } else { - self.destinationCodes = nil - self.destinations = destination?.tagValues(separatedBy: ",") - } - - self.exitCodes = exits?.tagValues(separatedBy: ";") - self.codes = ref?.tagValues(separatedBy: ";") - self.rotaryNames = rotaryName?.tagValues(separatedBy: ";") - } -} - -extension Road: Codable { - enum CodingKeys: String, CodingKey, CaseIterable { - case name - case ref - case exits - case destinations - case rotaryName = "rotary_name" - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - // Decoder apparently treats an empty string as a null value. - let name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" - let ref = try container.decodeIfPresent(String.self, forKey: .ref) - let exits = try container.decodeIfPresent(String.self, forKey: .exits) - let destinations = try container.decodeIfPresent(String.self, forKey: .destinations) - let rotaryName = try container.decodeIfPresent(String.self, forKey: .rotaryName) - self.init(name: name, ref: ref, exits: exits, destination: destinations, rotaryName: rotaryName) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - let ref = codes?.tagValues(joinedBy: ";") - if var name = names?.tagValues(joinedBy: ";") { - if let ref { - name = "\(name) (\(ref))" - } - try container.encodeIfPresent(name, forKey: .name) - } else { - try container.encode(ref ?? "", forKey: .name) - } - - if var destinations = destinations?.tagValues(joinedBy: ",") { - if let destinationCodes = destinationCodes?.tagValues(joinedBy: ",") { - destinations = "\(destinationCodes): \(destinations)" - } - try container.encode(destinations, forKey: .destinations) - } - - try container.encodeIfPresent(exitCodes?.tagValues(joinedBy: ";"), forKey: .exits) - try container.encodeIfPresent(ref, forKey: .ref) - try container.encodeIfPresent(rotaryNames?.tagValues(joinedBy: ";"), forKey: .rotaryName) - } -} - -/// A ``RouteStep`` object represents a single distinct maneuver along a route and the approach to the next maneuver. -/// The route step object corresponds to a single instruction the user must follow to complete a portion of the route. -/// For example, a step might require the user to turn then follow a road. -/// -/// You do not create instances of this class directly. Instead, you receive route step objects as part of route objects -/// when you request directions using the `Directions.calculate(_:completionHandler:)` method, setting the -/// ``DirectionsOptions/includesSteps`` option to `true` in the ``RouteOptions`` object that you pass into that method. -public struct RouteStep: Codable, ForeignMemberContainer, Equatable, Sendable { - public var foreignMembers: JSONObject = [:] - public var maneuverForeignMembers: JSONObject = [:] - - private enum CodingKeys: String, CodingKey, CaseIterable { - case shape = "geometry" - case distance - case drivingSide = "driving_side" - case expectedTravelTime = "duration" - case typicalTravelTime = "duration_typical" - case instructions - case instructionsDisplayedAlongStep = "bannerInstructions" - case instructionsSpokenAlongStep = "voiceInstructions" - case intersections - case maneuver - case pronunciation - case rotaryPronunciation = "rotary_pronunciation" - case speedLimitSignStandard = "speedLimitSign" - case speedLimitUnit - case transportType = "mode" - } - - private struct Maneuver: Codable, ForeignMemberContainer, Equatable, Sendable { - var foreignMembers: JSONObject = [:] - - private enum CodingKeys: String, CodingKey { - case instruction - case location - case type - case exitIndex = "exit" - case direction = "modifier" - case initialHeading = "bearing_before" - case finalHeading = "bearing_after" - } - - let instructions: String - let maneuverType: ManeuverType - let maneuverDirection: ManeuverDirection? - let maneuverLocation: Turf.LocationCoordinate2D - let initialHeading: Turf.LocationDirection? - let finalHeading: Turf.LocationDirection? - let exitIndex: Int? - - init( - instructions: String, - maneuverType: ManeuverType, - maneuverDirection: ManeuverDirection?, - maneuverLocation: Turf.LocationCoordinate2D, - initialHeading: Turf.LocationDirection?, - finalHeading: Turf.LocationDirection?, - exitIndex: Int? - ) { - self.instructions = instructions - self.maneuverType = maneuverType - self.maneuverLocation = maneuverLocation - self.maneuverDirection = maneuverDirection - self.initialHeading = initialHeading - self.finalHeading = finalHeading - self.exitIndex = exitIndex - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.maneuverLocation = try container.decode(LocationCoordinate2DCodable.self, forKey: .location) - .decodedCoordinates - self.maneuverType = (try? container.decode(ManeuverType.self, forKey: .type)) ?? .default - self.maneuverDirection = try container.decodeIfPresent(ManeuverDirection.self, forKey: .direction) - self.exitIndex = try container.decodeIfPresent(Int.self, forKey: .exitIndex) - - self.initialHeading = try container.decodeIfPresent(Turf.LocationDirection.self, forKey: .initialHeading) - self.finalHeading = try container.decodeIfPresent(Turf.LocationDirection.self, forKey: .finalHeading) - - if let instruction = try? container.decode(String.self, forKey: .instruction) { - self.instructions = instruction - } else { - self.instructions = "\(maneuverType) \(maneuverDirection?.rawValue ?? "")" - } - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(instructions, forKey: .instruction) - try container.encode(maneuverType, forKey: .type) - try container.encodeIfPresent(exitIndex, forKey: .exitIndex) - - try container.encodeIfPresent(maneuverDirection, forKey: .direction) - try container.encode(LocationCoordinate2DCodable(maneuverLocation), forKey: .location) - try container.encodeIfPresent(initialHeading, forKey: .initialHeading) - try container.encodeIfPresent(finalHeading, forKey: .finalHeading) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - } - - // MARK: Creating a Step - - /// Initializes a step. - /// - Parameters: - /// - transportType: The mode of transportation used for the step. - /// - maneuverLocation: The location of the maneuver at the beginning of this step. - /// - maneuverType: The type of maneuver required for beginning this step. - /// - maneuverDirection: Additional directional information to clarify the maneuver type. - /// - instructions: A string with instructions explaining how to perform the step’s maneuver. - /// - initialHeading: The user’s heading immediately before performing the maneuver. - /// - finalHeading: The user’s heading immediately after performing the maneuver. - /// - drivingSide: Indicates what side of a bidirectional road the driver must be driving on. Also referred to as - /// the rule of the road. - /// - exitCodes: Any [exit numbers](https://en.wikipedia.org/wiki/Exit_number) assigned to the highway exit at the - /// maneuver. - /// - exitNames: The names of the roundabout exit. - /// - phoneticExitNames: A phonetic or phonemic transcription indicating how to pronounce the names in the - /// ``exitNames`` property. - /// - distance: The step’s distance, measured in meters. - /// - expectedTravelTime: The step's expected travel time, measured in seconds. - /// - typicalTravelTime: The step's typical travel time, measured in seconds. - /// - names: The names of the road or path leading from this step’s maneuver to the next step’s maneuver. - /// - phoneticNames: A phonetic or phonemic transcription indicating how to pronounce the names in the ``names`` - /// property. - /// - codes: Any route reference codes assigned to the road or path leading from this step’s maneuver to the next - /// step’s maneuver. - /// - destinationCodes: Any route reference codes that appear on guide signage for the road leading from this - /// step’s maneuver to the next step’s maneuver. - /// - destinations: Destinations, such as [control cities](https://en.wikipedia.org/wiki/Control_city), that - /// appear on guide signage for the road leading from this step’s maneuver to the next step’s maneuver. - /// - intersections: An array of intersections along the step. - /// - speedLimitSignStandard: The sign design standard used for speed limit signs along the step. - /// - speedLimitUnit: The unit of speed limits on speed limit signs along the step. - /// - instructionsSpokenAlongStep: Instructions about the next step’s maneuver, optimized for speech synthesis. - /// - instructionsDisplayedAlongStep: Instructions about the next step’s maneuver, optimized for display in real - /// time. - /// - administrativeAreaContainerByIntersection: administrative region indices for each ``Intersection`` along the - /// step. - /// - segmentIndicesByIntersection: Segments indices for each ``Intersection`` along the step. - public init( - transportType: TransportType, - maneuverLocation: Turf.LocationCoordinate2D, - maneuverType: ManeuverType, - maneuverDirection: ManeuverDirection? = nil, - instructions: String, - initialHeading: Turf.LocationDirection? = nil, - finalHeading: Turf.LocationDirection? = nil, - drivingSide: DrivingSide, - exitCodes: [String]? = nil, - exitNames: [String]? = nil, - phoneticExitNames: [String]? = nil, - distance: Turf.LocationDistance, - expectedTravelTime: TimeInterval, - typicalTravelTime: TimeInterval? = nil, - names: [String]? = nil, - phoneticNames: [String]? = nil, - codes: [String]? = nil, - destinationCodes: [String]? = nil, - destinations: [String]? = nil, - intersections: [Intersection]? = nil, - speedLimitSignStandard: SignStandard? = nil, - speedLimitUnit: UnitSpeed? = nil, - instructionsSpokenAlongStep: [SpokenInstruction]? = nil, - instructionsDisplayedAlongStep: [VisualInstructionBanner]? = nil, - administrativeAreaContainerByIntersection: [Int?]? = nil, - segmentIndicesByIntersection: [Int?]? = nil - ) { - self.transportType = transportType - self.maneuverLocation = maneuverLocation - self.maneuverType = maneuverType - self.maneuverDirection = maneuverDirection - self.instructions = instructions - self.initialHeading = initialHeading - self.finalHeading = finalHeading - self.drivingSide = drivingSide - self.exitCodes = exitCodes - self.exitNames = exitNames - self.phoneticExitNames = phoneticExitNames - self.distance = distance - self.expectedTravelTime = expectedTravelTime - self.typicalTravelTime = typicalTravelTime - self.names = names - self.phoneticNames = phoneticNames - self.codes = codes - self.destinationCodes = destinationCodes - self.destinations = destinations - self.intersections = intersections - self.speedLimitSignStandard = speedLimitSignStandard - self.speedLimitUnit = speedLimitUnit - self.instructionsSpokenAlongStep = instructionsSpokenAlongStep - self.instructionsDisplayedAlongStep = instructionsDisplayedAlongStep - self.administrativeAreaContainerByIntersection = administrativeAreaContainerByIntersection - self.segmentIndicesByIntersection = segmentIndicesByIntersection - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(instructionsSpokenAlongStep, forKey: .instructionsSpokenAlongStep) - try container.encodeIfPresent(instructionsDisplayedAlongStep, forKey: .instructionsDisplayedAlongStep) - try container.encode(distance, forKey: .distance) - try container.encode(expectedTravelTime, forKey: .expectedTravelTime) - try container.encodeIfPresent(typicalTravelTime, forKey: .typicalTravelTime) - try container.encode(transportType, forKey: .transportType) - - let isRound = maneuverType == .takeRotary || maneuverType == .takeRoundabout - let road = Road( - names: isRound ? exitNames : names, - codes: codes, - exitCodes: exitCodes, - destinations: destinations, - destinationCodes: destinationCodes, - rotaryNames: isRound ? names : nil - ) - try road.encode(to: encoder) - if isRound { - try container.encodeIfPresent(phoneticNames?.tagValues(joinedBy: ";"), forKey: .rotaryPronunciation) - try container.encodeIfPresent(phoneticExitNames?.tagValues(joinedBy: ";"), forKey: .pronunciation) - } else { - try container.encodeIfPresent(phoneticNames?.tagValues(joinedBy: ";"), forKey: .pronunciation) - } - - if let intersectionsToEncode = intersections { - var intersectionsContainer = container.nestedUnkeyedContainer(forKey: .intersections) - try Intersection.encode( - intersections: intersectionsToEncode, - to: &intersectionsContainer, - administrativeRegionIndices: administrativeAreaContainerByIntersection, - segmentIndicesByIntersection: segmentIndicesByIntersection - ) - } - - try container.encode(drivingSide, forKey: .drivingSide) - if let shape { - let options = encoder.userInfo[.options] as? DirectionsOptions - let shapeFormat = options?.shapeFormat ?? .default - let polyLineString = PolyLineString(lineString: shape, shapeFormat: shapeFormat) - try container.encode(polyLineString, forKey: .shape) - } - - var maneuver = Maneuver( - instructions: instructions, - maneuverType: maneuverType, - maneuverDirection: maneuverDirection, - maneuverLocation: maneuverLocation, - initialHeading: initialHeading, - finalHeading: finalHeading, - exitIndex: exitIndex - ) - maneuver.foreignMembers = maneuverForeignMembers - try container.encode(maneuver, forKey: .maneuver) - - try container.encodeIfPresent(speedLimitSignStandard, forKey: .speedLimitSignStandard) - if let speedLimitUnit, - let unit = SpeedLimitDescriptor.UnitDescriptor(unit: speedLimitUnit) - { - try container.encode(unit, forKey: .speedLimitUnit) - } - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - static func decode(from decoder: Decoder, administrativeRegions: [AdministrativeRegion]) throws -> [RouteStep] { - var container = try decoder.unkeyedContainer() - - var steps = [RouteStep]() - while !container.isAtEnd { - let step = try RouteStep(from: container.superDecoder(), administrativeRegions: administrativeRegions) - - steps.append(step) - } - - return steps - } - - /// Used to Decode `Intersection.admin_index` - private struct AdministrativeAreaIndex: Codable, Sendable { - private enum CodingKeys: String, CodingKey { - case administrativeRegionIndex = "admin_index" - } - - var administrativeRegionIndex: Int? - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.administrativeRegionIndex = try container.decodeIfPresent(Int.self, forKey: .administrativeRegionIndex) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(administrativeRegionIndex, forKey: .administrativeRegionIndex) - } - } - - /// Used to Decode `Intersection.geometry_index` - private struct IntersectionShapeIndex: Codable, Sendable { - private enum CodingKeys: String, CodingKey { - case geometryIndex = "geometry_index" - } - - let geometryIndex: Int? - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.geometryIndex = try container.decodeIfPresent(Int.self, forKey: .geometryIndex) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(geometryIndex, forKey: .geometryIndex) - } - } - - public init(from decoder: Decoder) throws { - try self.init(from: decoder, administrativeRegions: nil) - } - - init(from decoder: Decoder, administrativeRegions: [AdministrativeRegion]?) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let maneuver = try container.decode(Maneuver.self, forKey: .maneuver) - - self.maneuverLocation = maneuver.maneuverLocation - self.maneuverType = maneuver.maneuverType - self.maneuverDirection = maneuver.maneuverDirection - self.exitIndex = maneuver.exitIndex - self.initialHeading = maneuver.initialHeading - self.finalHeading = maneuver.finalHeading - self.instructions = maneuver.instructions - self.maneuverForeignMembers = maneuver.foreignMembers - - if let polyLineString = try container.decodeIfPresent(PolyLineString.self, forKey: .shape) { - self.shape = try LineString(polyLineString: polyLineString) - } else { - self.shape = nil - } - - self.drivingSide = try container.decode(DrivingSide.self, forKey: .drivingSide) - - self.instructionsSpokenAlongStep = try container.decodeIfPresent( - [SpokenInstruction].self, - forKey: .instructionsSpokenAlongStep - ) - - if var visuals = try container.decodeIfPresent( - [VisualInstructionBanner].self, - forKey: .instructionsDisplayedAlongStep - ) { - for index in visuals.indices { - visuals[index].drivingSide = drivingSide - } - - self.instructionsDisplayedAlongStep = visuals - } else { - self.instructionsDisplayedAlongStep = nil - } - - self.distance = try container.decode(Turf.LocationDirection.self, forKey: .distance) - self.expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime) - self.typicalTravelTime = try container.decodeIfPresent(TimeInterval.self, forKey: .typicalTravelTime) - - self.transportType = try container.decode(TransportType.self, forKey: .transportType) - self.administrativeAreaContainerByIntersection = try container.decodeIfPresent( - [AdministrativeAreaIndex].self, - forKey: .intersections - )? - .map(\.administrativeRegionIndex) - var rawIntersections = try container.decodeIfPresent([Intersection].self, forKey: .intersections) - - // Updating `Intersection.regionCode` since we removed it's `admin_index` for convenience - if let administrativeRegions, - rawIntersections != nil, - let rawAdminIndicies = administrativeAreaContainerByIntersection - { - for index in 0.. regionIndex - { - rawIntersections![index].updateRegionCode(administrativeRegions[regionIndex].countryCode) - } - } - } - - self.intersections = rawIntersections - - self.segmentIndicesByIntersection = try container.decodeIfPresent( - [IntersectionShapeIndex].self, - forKey: .intersections - )?.map(\.geometryIndex) - - let road = try Road(from: decoder) - self.codes = road.codes - self.exitCodes = road.exitCodes - self.destinations = road.destinations - self.destinationCodes = road.destinationCodes - - self.speedLimitSignStandard = try container.decodeIfPresent(SignStandard.self, forKey: .speedLimitSignStandard) - self.speedLimitUnit = try (container.decodeIfPresent( - SpeedLimitDescriptor.UnitDescriptor.self, - forKey: .speedLimitUnit - ))?.describedUnit - - let type = maneuverType - if type == .takeRotary || type == .takeRoundabout { - self.names = road.rotaryNames - self.phoneticNames = try container.decodeIfPresent(String.self, forKey: .rotaryPronunciation)? - .tagValues(separatedBy: ";") - self.exitNames = road.names - self.phoneticExitNames = try container.decodeIfPresent(String.self, forKey: .pronunciation)? - .tagValues(separatedBy: ";") - } else { - self.names = road.names - self.phoneticNames = try container.decodeIfPresent(String.self, forKey: .pronunciation)? - .tagValues(separatedBy: ";") - self.exitNames = nil - self.phoneticExitNames = nil - } - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - try decodeForeignMembers(notKeyedBy: Road.CodingKeys.self, with: decoder) - } - - // MARK: Getting the Shape of the Step - - /// The path of the route step from the location of the maneuver to the location of the next step’s maneuver. - /// - /// The value of this property may be `nil`, for example when the maneuver type is ``ManeuverType/arrive``. - /// - /// Using the [Mapbox Maps SDK for iOS](https://www.mapbox.com/ios-sdk/) or [Mapbox Maps SDK for - /// macOS](https://github.com/mapbox/mapbox-gl-native/tree/master/platform/macos/), you can create an `MGLPolyline` - /// object using the `LineString.coordinates` property to display a portion of a route on an `MGLMapView`. - public var shape: LineString? - - // MARK: Getting the Mode of Transportation - - /// The mode of transportation used for the step. - /// - /// This step may use a different mode of transportation than the overall route. - public let transportType: TransportType - - // MARK: Getting Details About the Maneuver - - /// The location of the maneuver at the beginning of this step. - public let maneuverLocation: Turf.LocationCoordinate2D - - /// The type of maneuver required for beginning this step. - public let maneuverType: ManeuverType - - /// Additional directional information to clarify the maneuver type. - public let maneuverDirection: ManeuverDirection? - - /// A string with instructions explaining how to perform the step’s maneuver. - /// - /// You can display this string or read it aloud to the user. The string does not include the distance to or from - /// the maneuver. For instructions optimized for real-time delivery during turn-by-turn navigation, set the - /// ``DirectionsOptions/includesSpokenInstructions`` option and use the ``instructionsSpokenAlongStep`` property. If - /// you need customized instructions, you can construct them yourself from the step’s other properties or use [OSRM - /// Text Instructions](https://github.com/Project-OSRM/osrm-text-instructions.swift/). - /// - /// - Note: If you use the MapboxDirections framework with the Mapbox Directions API, this property is formatted and - /// localized for display to the user. If you use OSRM directly, this property contains a basic string that only - /// includes the maneuver type and direction. Use [OSRM Text - /// Instructions](https://github.com/Project-OSRM/osrm-text-instructions.swift/) to construct a complete, localized - /// instruction string for display. - public let instructions: String - - /// The user’s heading immediately before performing the maneuver. - public let initialHeading: Turf.LocationDirection? - - /// The user’s heading immediately after performing the maneuver. - /// - /// The value of this property may differ from the user’s heading after traveling along the road past the maneuver. - public let finalHeading: Turf.LocationDirection? - - /// Indicates what side of a bidirectional road the driver must be driving on. Also referred to as the rule of the - /// road. - public let drivingSide: DrivingSide - - /// The number of exits from the previous maneuver up to and including this step’s maneuver. - /// - /// If the maneuver takes place on a surface street, this property counts intersections. The number of intersections - /// does not necessarily correspond to the number of blocks. If the maneuver takes place on a grade-separated - /// highway (freeway or motorway), this property counts highway exits but not highway entrances. If the maneuver is - /// a roundabout maneuver, the exit index is the number of exits from the approach to the recommended outlet. For - /// the signposted exit numbers associated with a highway exit, use the ``exitCodes`` property. - /// - /// In some cases, the number of exits leading to a maneuver may be more useful to the user than the distance to the - /// maneuver. - public var exitIndex: Int? - - /// Any [exit numbers](https://en.wikipedia.org/wiki/Exit_number) assigned to the highway exit at the maneuver. - /// - /// This property is only set when the ``maneuverType`` is ``ManeuverType/takeOffRamp``. For the number of exits - /// from the previous maneuver, regardless of the highway’s exit numbering scheme, use the ``exitIndex`` property. - /// For the route reference codes associated with the connecting road, use the ``destinationCodes`` property. For - /// the names associated with a roundabout exit, use the ``exitNames`` property. - /// - /// An exit number is an alphanumeric identifier posted at or ahead of a highway off-ramp. Exit numbers may increase - /// or decrease sequentially along a road, or they may correspond to distances from either end of the road. An - /// alphabetic suffix may appear when multiple exits are located in the same interchange. If multiple exits are - /// [combined into a single - /// exit](https://en.wikipedia.org/wiki/Local-express_lanes#Example_of_cloverleaf_interchanges), the step may have - /// multiple exit codes. - public let exitCodes: [String]? - - /// The names of the roundabout exit. - /// - /// This property is only set for roundabout (traffic circle or rotary) maneuvers. For the signposted names - /// associated with a highway exit, use the ``destinations`` property. For the signposted exit numbers, use the - /// ``exitCodes`` property. - /// - /// If you display a name to the user, you may need to abbreviate common words like “East” or “Boulevard” to ensure - /// that it fits in the allotted space. - public let exitNames: [String]? - - /// A phonetic or phonemic transcription indicating how to pronounce the names in the ``exitNames`` property. - /// - /// This property is only set for roundabout (traffic circle or rotary) maneuvers. - /// - /// The transcription is written in the [International Phonetic - /// Alphabet](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet). - public let phoneticExitNames: [String]? - - // MARK: Getting Details About the Approach to the Next Maneuver - - /// The step’s distance, measured in meters. - /// - /// The value of this property accounts for the distance that the user must travel to go from this step’s maneuver - /// location to the next step’s maneuver location. It is not the sum of the direct distances between the route’s - /// waypoints, nor should you assume that the user would travel along this distance at a fixed speed. - public let distance: Turf.LocationDistance - - /// The step’s expected travel time, measured in seconds. - /// - /// The value of this property reflects the time it takes to go from this step’s maneuver location to the next - /// step’s maneuver location. If the route was calculated using the ``ProfileIdentifier/automobileAvoidingTraffic`` - /// profile, this property reflects current traffic conditions at the time of the request, not necessarily the - /// traffic conditions at the time the user would begin this step. For other profiles, this property reflects travel - /// time under ideal conditions and does not account for traffic congestion. If the step makes use of a ferry or - /// train, the actual travel time may additionally be subject to the schedules of those services. - /// - /// Do not assume that the user would travel along the step at a fixed speed. For the expected travel time on each - /// individual segment along the leg, specify the ``AttributeOptions/expectedTravelTime`` option and use the - /// ``RouteLeg/expectedSegmentTravelTimes`` property. - public var expectedTravelTime: TimeInterval - - /// The step’s typical travel time, measured in seconds. - /// - /// The value of this property reflects the typical time it takes to go from this step’s maneuver location to the - /// next step’s maneuver location. This property is available when using the - /// ``ProfileIdentifier/automobileAvoidingTraffic`` profile. This property reflects typical traffic conditions at - /// the time of the request, not necessarily the typical traffic conditions at the time the user would begin this - /// step. If the step makes use of a ferry, the typical travel time may additionally be subject to the schedule of - /// this service. - /// - /// Do not assume that the user would travel along the step at a fixed speed. - public var typicalTravelTime: TimeInterval? - - /// The names of the road or path leading from this step’s maneuver to the next step’s maneuver. - /// - /// If the maneuver is a turning maneuver, the step’s names are the name of the road or path onto which the user - /// turns. If you display a name to the user, you may need to abbreviate common words like “East” or “Boulevard” to - /// ensure that it fits in the allotted space. - /// - /// If the maneuver is a roundabout maneuver, the outlet to take is named in the ``exitNames`` property; the - /// ``names`` property is only set for large roundabouts that have their own names. - public let names: [String]? - - /// A phonetic or phonemic transcription indicating how to pronounce the names in the `names` property. - /// - /// The transcription is written in the [International Phonetic - /// Alphabet](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet). - /// - /// If the maneuver traverses a large, named roundabout, this property contains a hint about how to pronounce the - /// names of the outlet to take. - public let phoneticNames: [String]? - - /// Any route reference codes assigned to the road or path leading from this step’s maneuver to the next step’s - /// maneuver. - /// - /// A route reference code commonly consists of an alphabetic network code, a space or hyphen, and a route number. - /// You should not assume that the network code is globally unique: for example, a network code of “NH” may indicate - /// a “National Highway” or “New Hampshire”. Moreover, a route number may not even uniquely identify a route within - /// a given network. - /// - /// If a highway ramp is part of a numbered route, its reference code is contained in this property. On the other - /// hand, guide signage for a highway ramp usually indicates route reference codes of the adjoining road; use the - /// ``destinationCodes`` property for those route reference codes. - public let codes: [String]? - - /// Any route reference codes that appear on guide signage for the road leading from this step’s maneuver to the - /// next step’s maneuver. - /// - /// This property is typically available in steps leading to or from a freeway or expressway. This property contains - /// route reference codes associated with a road later in the route. If a highway ramp is itself part of a numbered - /// route, its reference code is contained in the `codes` property. For the signposted exit numbers associated with - /// a highway exit, use the `exitCodes` property. - /// - /// A route reference code commonly consists of an alphabetic network code, a space or hyphen, and a route number. - /// You should not assume that the network code is globally unique: for example, a network code of “NH” may indicate - /// a “National Highway” or “New Hampshire”. Moreover, a route number may not even uniquely identify a route within - /// a given network. A destination code for a divided road is often suffixed with the cardinal direction of travel, - /// for example “I 80 East”. - public let destinationCodes: [String]? - - /// Destinations, such as [control cities](https://en.wikipedia.org/wiki/Control_city), that appear on guide signage - /// for the road leading from this step’s maneuver to the next step’s maneuver. - /// - /// This property is typically available in steps leading to or from a freeway or expressway. - public let destinations: [String]? - - /// An array of intersections along the step. - /// - /// Each item in the array corresponds to a cross street, starting with the intersection at the maneuver location - /// indicated by the coordinates property and continuing with each cross street along the step. - public let intersections: [Intersection]? - - /// Each intersection’s administrative region index. - /// - /// This property is set to `nil` if the ``intersections`` property is `nil`. An individual array element may be - /// `nil` if the corresponding ``Intersection`` instance has no administrative region assigned. - /// - SeeAlso: ``Intersection/regionCode``, ``RouteLeg/regionCode(atStepIndex:intersectionIndex:)`` - public let administrativeAreaContainerByIntersection: [Int?]? - - /// Segments indices for each ``Intersection`` along the step. - /// - /// The indices are arranged in the same order as the items of ``intersections``. This property is `nil` if - /// ``intersections`` is `nil`. An individual item may be `nil` if the corresponding JSON-formatted intersection - /// object has no `geometry_index` property. - public let segmentIndicesByIntersection: [Int?]? - - /// The sign design standard used for speed limit signs along the step. - /// - /// This standard affects how corresponding speed limits in the ``RouteLeg/segmentMaximumSpeedLimits`` property - /// should be displayed. - public let speedLimitSignStandard: SignStandard? - - /// The unit of speed limits on speed limit signs along the step. - /// - /// This standard affects how corresponding speed limits in the ``RouteLeg/segmentMaximumSpeedLimits`` property - /// should be displayed. - public let speedLimitUnit: UnitSpeed? - - // MARK: Getting Details About the Next Maneuver - - /// Instructions about the next step’s maneuver, optimized for speech synthesis. - /// - /// As the user traverses this step, you can give them advance notice of the upcoming maneuver by reading aloud each - /// item in this array in order as the user reaches the specified distances along this step. The text of the spoken - /// instructions refers to the details in the next step, but the distances are measured from the beginning of this - /// step. - /// - /// This property is non-`nil` if the ``DirectionsOptions/includesSpokenInstructions`` option is set to `true`. For - /// instructions designed for display, use the ``instructions`` property. - public let instructionsSpokenAlongStep: [SpokenInstruction]? - - /// Instructions about the next step’s maneuver, optimized for display in real time. - /// - /// As the user traverses this step, you can give them advance notice of the upcoming maneuver by displaying each - /// item in this array in order as the user reaches the specified distances along this step. The text and images of - /// the visual instructions refer to the details in the next step, but the distances are measured from the beginning - /// of this step. - /// - /// This property is non-`nil` if the ``DirectionsOptions/includesVisualInstructions`` option is set to `true`. For - /// instructions designed for speech synthesis, use the ``instructionsSpokenAlongStep`` property. For instructions - /// designed for display in a static list, use the ``instructions`` property. - public let instructionsDisplayedAlongStep: [VisualInstructionBanner]? -} - -extension RouteStep: CustomStringConvertible { - public var description: String { - return instructions - } -} - -extension RouteStep: CustomQuickLookConvertible { - func debugQuickLookObject() -> Any? { - guard let shape else { - return nil - } - return debugQuickLookURL(illustrating: shape) - } -} - -extension RouteStep { - public static func == (lhs: RouteStep, rhs: RouteStep) -> Bool { - // Compare all the properties, from cheapest to most expensive to compare. - return lhs.initialHeading == rhs.initialHeading && - lhs.finalHeading == rhs.finalHeading && - lhs.instructions == rhs.instructions && - lhs.exitIndex == rhs.exitIndex && - lhs.distance == rhs.distance && - lhs.expectedTravelTime == rhs.expectedTravelTime && - lhs.typicalTravelTime == rhs.typicalTravelTime && - - lhs.maneuverType == rhs.maneuverType && - lhs.maneuverDirection == rhs.maneuverDirection && - lhs.drivingSide == rhs.drivingSide && - lhs.transportType == rhs.transportType && - - lhs.maneuverLocation == rhs.maneuverLocation && - - lhs.exitCodes == rhs.exitCodes && - lhs.exitNames == rhs.exitNames && - lhs.phoneticExitNames == rhs.phoneticExitNames && - lhs.names == rhs.names && - lhs.phoneticNames == rhs.phoneticNames && - lhs.codes == rhs.codes && - lhs.destinationCodes == rhs.destinationCodes && - lhs.destinations == rhs.destinations && - - lhs.speedLimitSignStandard == rhs.speedLimitSignStandard && - lhs.speedLimitUnit == rhs.speedLimitUnit && - - lhs.intersections == rhs.intersections && - lhs.instructionsSpokenAlongStep == rhs.instructionsSpokenAlongStep && - lhs.instructionsDisplayedAlongStep == rhs.instructionsDisplayedAlongStep && - - lhs.shape == rhs.shape - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/SilentWaypoint.swift b/ios/Classes/Navigation/MapboxDirections/SilentWaypoint.swift deleted file mode 100644 index bbd9b4955..000000000 --- a/ios/Classes/Navigation/MapboxDirections/SilentWaypoint.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation -import Turf - -/// Represents a silent waypoint along the ``RouteLeg``. -/// -/// See ``RouteLeg/viaWaypoints`` for more details. -public struct SilentWaypoint: Codable, Equatable, ForeignMemberContainer, Sendable { - public var foreignMembers: JSONObject = [:] - - public enum CodingKeys: String, CodingKey { - case waypointIndex = "waypoint_index" - case distanceFromStart = "distance_from_start" - case shapeCoordinateIndex = "geometry_index" - } - - /// The associated waypoint index in `RouteResponse.waypoints`, excluding the origin (index 0) and destination. - public var waypointIndex: Int - - /// The calculated distance, in meters, from the leg origin. - public var distanceFromStart: Double - - /// The associated ``Route`` shape index of the silent waypoint location. - public var shapeCoordinateIndex: Int - - public init(waypointIndex: Int, distanceFromStart: Double, shapeCoordinateIndex: Int) { - self.waypointIndex = waypointIndex - self.distanceFromStart = distanceFromStart - self.shapeCoordinateIndex = shapeCoordinateIndex - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.waypointIndex = try container.decode(Int.self, forKey: .waypointIndex) - self.distanceFromStart = try container.decode(Double.self, forKey: .distanceFromStart) - self.shapeCoordinateIndex = try container.decode(Int.self, forKey: .shapeCoordinateIndex) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(waypointIndex, forKey: .waypointIndex) - try container.encode(distanceFromStart, forKey: .distanceFromStart) - try container.encode(shapeCoordinateIndex, forKey: .shapeCoordinateIndex) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/SpokenInstruction.swift b/ios/Classes/Navigation/MapboxDirections/SpokenInstruction.swift deleted file mode 100644 index fdf813784..000000000 --- a/ios/Classes/Navigation/MapboxDirections/SpokenInstruction.swift +++ /dev/null @@ -1,83 +0,0 @@ -import Foundation -import Turf - -/// An instruction about an upcoming ``RouteStep``’s maneuver, optimized for speech synthesis. -/// -/// The instruction is provided in two formats: plain text and text marked up according to the [Speech Synthesis Markup -/// Language](https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language) (SSML). Use a speech synthesizer such as -/// `AVSpeechSynthesizer` or Amazon Polly to read aloud the instruction. -/// -/// The ``SpokenInstruction/distanceAlongStep`` property is measured from the beginning of the step associated with this -/// object. By contrast, the `text` and `ssmlText` properties refer to the details in the following step. It is also -/// possible for the instruction to refer to two following steps simultaneously when needed for safe navigation. -public struct SpokenInstruction: Codable, ForeignMemberContainer, Equatable, Sendable { - public var foreignMembers: JSONObject = [:] - - private enum CodingKeys: String, CodingKey, CaseIterable { - case distanceAlongStep = "distanceAlongGeometry" - case text = "announcement" - case ssmlText = "ssmlAnnouncement" - } - - // MARK: Creating a Spoken Instruction - - /// Initialize a spoken instruction. - /// - Parameters: - /// - distanceAlongStep: A distance along the associated ``RouteStep`` at which to read the instruction aloud. - /// - text: A plain-text representation of the speech-optimized instruction. - /// - ssmlText: A formatted representation of the speech-optimized instruction. - public init(distanceAlongStep: LocationDistance, text: String, ssmlText: String) { - self.distanceAlongStep = distanceAlongStep - self.text = text - self.ssmlText = ssmlText - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.distanceAlongStep = try container.decode(LocationDistance.self, forKey: .distanceAlongStep) - self.text = try container.decode(String.self, forKey: .text) - self.ssmlText = try container.decode(String.self, forKey: .ssmlText) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(distanceAlongStep, forKey: .distanceAlongStep) - try container.encode(text, forKey: .text) - try container.encode(ssmlText, forKey: .ssmlText) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - // MARK: Timing When to Say the Instruction - - /// A distance along the associated ``RouteStep`` at which to read the instruction aloud. - /// - /// The distance is measured in meters from the beginning of the associated step. - public let distanceAlongStep: LocationDistance - - // MARK: Getting the Instruction to Say - - /// A plain-text representation of the speech-optimized instruction. - /// This representation is appropriate for speech synthesizers that lack support for the [Speech Synthesis Markup - /// Language](https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language) (SSML), such as `AVSpeechSynthesizer`. - /// For speech synthesizers that support SSML, use the ``ssmlText`` property instead. - public let text: String - - /// A formatted representation of the speech-optimized instruction. - /// - /// This representation is appropriate for speech synthesizers that support the [Speech Synthesis Markup - /// Language](https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language) (SSML), such as [Amazon - /// Polly](https://aws.amazon.com/polly/). Numbers and names are marked up to ensure correct pronunciation. For - /// speech synthesizers that lack SSML support, use the ``text`` property instead. - public let ssmlText: String -} - -extension SpokenInstruction { - public static func == (lhs: SpokenInstruction, rhs: SpokenInstruction) -> Bool { - return lhs.distanceAlongStep == rhs.distanceAlongStep && - lhs.text == rhs.text && - lhs.ssmlText == rhs.ssmlText - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/TollCollection.swift b/ios/Classes/Navigation/MapboxDirections/TollCollection.swift deleted file mode 100644 index 953504a67..000000000 --- a/ios/Classes/Navigation/MapboxDirections/TollCollection.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation -import Turf - -/// `TollCollection` describes corresponding object on the route. -public struct TollCollection: Codable, Equatable, ForeignMemberContainer, Sendable { - public var foreignMembers: JSONObject = [:] - - public enum CollectionType: String, Codable, Sendable { - case booth = "toll_booth" - case gantry = "toll_gantry" - } - - /// The type of the toll collection point. - public let type: CollectionType - - /// The name of the toll collection point. - public var name: String? - - private enum CodingKeys: String, CodingKey { - case type - case name - } - - public init(type: CollectionType) { - self.init(type: type, name: nil) - } - - public init(type: CollectionType, name: String?) { - self.type = type - self.name = name - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.type = try container.decode(CollectionType.self, forKey: .type) - self.name = try container.decodeIfPresent(String.self, forKey: .name) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type, forKey: .type) - try container.encodeIfPresent(name, forKey: .name) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - public static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.type == rhs.type - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/TollPrice.swift b/ios/Classes/Navigation/MapboxDirections/TollPrice.swift deleted file mode 100644 index d4466904a..000000000 --- a/ios/Classes/Navigation/MapboxDirections/TollPrice.swift +++ /dev/null @@ -1,147 +0,0 @@ -import Foundation -import Turf - -/// Information about toll payment method. -public struct TollPaymentMethod: Hashable, Equatable, Sendable { - /// Method identifier. - public let identifier: String - - /// Payment is done by electronic toll collection. - public static let electronicTollCollection = TollPaymentMethod(identifier: "etc") - /// Payment is done by cash. - public static let cash = TollPaymentMethod(identifier: "cash") -} - -/// Categories by which toll fees are divided. -public struct TollCategory: Hashable, Equatable, Sendable { - /// Category name. - public let name: String - - /// A small sized vehicle. - /// - /// In Japan, this is a [standard vehicle size](https://en.wikipedia.org/wiki/Expressways_of_Japan#Tolls). - public static let small = TollCategory(name: "small") - /// A standard sized vehicle. - /// - /// In Japan, this is a [standard vehicle size](https://en.wikipedia.org/wiki/Expressways_of_Japan#Tolls). - public static let standard = TollCategory(name: "standard") - /// A middle sized vehicle. - /// - /// In Japan, this is a [standard vehicle size](https://en.wikipedia.org/wiki/Expressways_of_Japan#Tolls). - public static let middle = TollCategory(name: "middle") - /// A large sized vehicle. - /// - /// In Japan, this is a [standard vehicle size](https://en.wikipedia.org/wiki/Expressways_of_Japan#Tolls). - public static let large = TollCategory(name: "large") - /// A jumbo sized vehicle. - /// - /// In Japan, this is a [standard vehicle size](https://en.wikipedia.org/wiki/Expressways_of_Japan#Tolls). - public static let jumbo = TollCategory(name: "jumbo") -} - -/// Toll cost information for the ``Route``. -public struct TollPrice: Equatable, Hashable, ForeignMemberContainer, Sendable { - public var foreignMembers: Turf.JSONObject = [:] - - /// Related currency code string. - /// - /// Uses ISO 4217 format. Refers to ``amount`` value. - /// This value is compatible with `NumberFormatter().currencyCode`. - public let currencyCode: String - /// Information about toll payment. - public let paymentMethod: TollPaymentMethod - /// Toll category information. - public let category: TollCategory - /// The actual toll price in ``currencyCode`` currency. - /// - /// A toll cost of `0` is valid and simply means that no toll costs are incurred for this route. - public let amount: Decimal - - init(currencyCode: String, paymentMethod: TollPaymentMethod, category: TollCategory, amount: Decimal) { - self.currencyCode = currencyCode - self.paymentMethod = paymentMethod - self.category = category - self.amount = amount - } -} - -struct TollPriceCoder: Codable, Sendable { - let tollPrices: [TollPrice] - - init(tollPrices: [TollPrice]) { - self.tollPrices = tollPrices - } - - private class TollPriceItem: Codable, ForeignMemberContainerClass { - var foreignMembers: Turf.JSONObject = [:] - - private enum CodingKeys: String, CodingKey, CaseIterable { - case currency - case paymentMethods = "payment_methods" - } - - var currencyCode: String - var paymentMethods: [String: [String: Decimal]] = [:] - - init(currencyCode: String) { - self.currencyCode = currencyCode - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.currencyCode = try container.decode(String.self, forKey: .currency) - self.paymentMethods = try container.decode([String: [String: Decimal]].self, forKey: .paymentMethods) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(currencyCode, forKey: .currency) - try container.encode(paymentMethods, forKey: .paymentMethods) - - try encodeForeignMembers(to: encoder) - } - } - - init(from decoder: Decoder) throws { - let item = try TollPriceItem(from: decoder) - - var tollPrices = [TollPrice]() - for method in item.paymentMethods { - for category in method.value { - var newItem = TollPrice( - currencyCode: item.currencyCode, - paymentMethod: TollPaymentMethod(identifier: method.key), - category: TollCategory(name: category.key), - amount: category.value - ) - newItem.foreignMembers = item.foreignMembers - tollPrices.append(newItem) - } - } - self.tollPrices = tollPrices - } - - func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() - var items: [TollPriceItem] = [] - - for price in tollPrices { - var item: TollPriceItem - if let existingItem = items.first(where: { $0.currencyCode == price.currencyCode }) { - item = existingItem - } else { - item = TollPriceItem(currencyCode: price.currencyCode) - item.foreignMembers = price.foreignMembers - items.append(item) - } - if item.paymentMethods[price.paymentMethod.identifier] == nil { - item.paymentMethods[price.paymentMethod.identifier] = [:] - } - item.paymentMethods[price.paymentMethod.identifier]?[price.category.name] = price.amount - } - - try container.encode(contentsOf: items) - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/TrafficTendency.swift b/ios/Classes/Navigation/MapboxDirections/TrafficTendency.swift deleted file mode 100644 index d0fa7f77e..000000000 --- a/ios/Classes/Navigation/MapboxDirections/TrafficTendency.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -/// :nodoc: -/// The tendency value conveys the changing state of traffic congestion (increasing, decreasing, constant etc). -/// -/// New values could be introduced in the future without an API version change. -public enum TrafficTendency: Int, Codable, CaseIterable, Equatable, Sendable { - /// Congestion tendency is unknown. - case unknown = 0 - /// Congestion tendency is not changing. - case constant = 1 - /// Congestion tendency is increasing. - case increasing = 2 - /// Congestion tendency is decreasing. - case decreasing = 3 - /// Congestion tendency is rapidly increasing. - case rapidlyIncreasing = 4 - /// Congestion tendency is rapidly decreasing. - case rapidlyDecreasing = 5 -} diff --git a/ios/Classes/Navigation/MapboxDirections/VisualInstruction.swift b/ios/Classes/Navigation/MapboxDirections/VisualInstruction.swift deleted file mode 100644 index b915c0e11..000000000 --- a/ios/Classes/Navigation/MapboxDirections/VisualInstruction.swift +++ /dev/null @@ -1,95 +0,0 @@ -import Foundation -import Turf - -/// The contents of a banner that should be displayed as added visual guidance for a route. The banner instructions are -/// children of the steps during which they should be displayed, but they refer to the maneuver in the following step. -public struct VisualInstruction: Codable, ForeignMemberContainer, Equatable, Sendable { - public var foreignMembers: JSONObject = [:] - - // MARK: Creating a Visual Instruction - - private enum CodingKeys: String, CodingKey, CaseIterable { - case text - case maneuverType = "type" - case maneuverDirection = "modifier" - case components - case finalHeading = "degrees" - } - - /// Initializes a new visual instruction banner object that displays the given information. - public init( - text: String?, - maneuverType: ManeuverType?, - maneuverDirection: ManeuverDirection?, - components: [Component], - degrees: LocationDegrees? = nil - ) { - self.text = text - self.maneuverType = maneuverType - self.maneuverDirection = maneuverDirection - self.components = components - self.finalHeading = degrees - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(text, forKey: .text) - try container.encodeIfPresent(maneuverType, forKey: .maneuverType) - try container.encodeIfPresent(maneuverDirection, forKey: .maneuverDirection) - try container.encode(components, forKey: .components) - try container.encodeIfPresent(finalHeading, forKey: .finalHeading) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.text = try container.decodeIfPresent(String.self, forKey: .text) - self.maneuverType = try container.decodeIfPresent(ManeuverType.self, forKey: .maneuverType) - self.maneuverDirection = try container.decodeIfPresent(ManeuverDirection.self, forKey: .maneuverDirection) - self.components = try container.decode([Component].self, forKey: .components) - self.finalHeading = try container.decodeIfPresent(LocationDegrees.self, forKey: .finalHeading) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - // MARK: Displaying the Instruction Text - - /// A plain text representation of the instruction. - /// - /// This property is set to `nil` when the ``text`` property in the Mapbox Directions API response is an empty - /// string. - public let text: String? - - /// A structured representation of the instruction. - public let components: [Component] - - // MARK: Displaying a Maneuver Image - - /// The type of maneuver required for beginning the step described by the visual instruction. - public var maneuverType: ManeuverType? - - /// Additional directional information to clarify the maneuver type. - public var maneuverDirection: ManeuverDirection? - - /// The heading at which the user exits a roundabout (traffic circle or rotary). - /// - /// This property is measured in degrees clockwise relative to the user’s initial heading. A value of 180° means - /// continuing through the roundabout without changing course, whereas a value of 0° means traversing the entire - /// roundabout back to the entry point. - /// - /// This property is only relevant if the ``maneuverType`` is any of the following values: - /// ``ManeuverType/takeRoundabout``, ``ManeuverType/takeRotary``, ``ManeuverType/turnAtRoundabout``, - /// ``ManeuverType/exitRoundabout``, or ``ManeuverType/exitRotary``. - public var finalHeading: LocationDegrees? -} - -extension VisualInstruction { - public static func == (lhs: VisualInstruction, rhs: VisualInstruction) -> Bool { - return lhs.text == rhs.text && - lhs.maneuverType == rhs.maneuverType && - lhs.maneuverDirection == rhs.maneuverDirection && - lhs.components == rhs.components && - lhs.finalHeading == rhs.finalHeading - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/VisualInstructionBanner.swift b/ios/Classes/Navigation/MapboxDirections/VisualInstructionBanner.swift deleted file mode 100644 index 1ddc572f7..000000000 --- a/ios/Classes/Navigation/MapboxDirections/VisualInstructionBanner.swift +++ /dev/null @@ -1,112 +0,0 @@ -import Foundation -import Turf - -extension CodingUserInfoKey { - static let drivingSide = CodingUserInfoKey(rawValue: "drivingSide")! -} - -/// A visual instruction banner contains all the information necessary for creating a visual cue about a given -/// ``RouteStep``. -public struct VisualInstructionBanner: Codable, ForeignMemberContainer, Equatable, Sendable { - public var foreignMembers: JSONObject = [:] - - private enum CodingKeys: String, CodingKey, CaseIterable { - case distanceAlongStep = "distanceAlongGeometry" - case primaryInstruction = "primary" - case secondaryInstruction = "secondary" - case tertiaryInstruction = "sub" - case quaternaryInstruction = "view" - case drivingSide - } - - // MARK: Creating a Visual Instruction Banner - - /// Initializes a visual instruction banner with the given instructions. - public init( - distanceAlongStep: LocationDistance, - primary: VisualInstruction, - secondary: VisualInstruction?, - tertiary: VisualInstruction?, - quaternary: VisualInstruction?, - drivingSide: DrivingSide - ) { - self.distanceAlongStep = distanceAlongStep - self.primaryInstruction = primary - self.secondaryInstruction = secondary - self.tertiaryInstruction = tertiary - self.quaternaryInstruction = quaternary - self.drivingSide = drivingSide - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(distanceAlongStep, forKey: .distanceAlongStep) - try container.encode(primaryInstruction, forKey: .primaryInstruction) - try container.encodeIfPresent(secondaryInstruction, forKey: .secondaryInstruction) - try container.encodeIfPresent(tertiaryInstruction, forKey: .tertiaryInstruction) - try container.encodeIfPresent(quaternaryInstruction, forKey: .quaternaryInstruction) - try container.encode(drivingSide, forKey: .drivingSide) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.distanceAlongStep = try container.decode(LocationDistance.self, forKey: .distanceAlongStep) - self.primaryInstruction = try container.decode(VisualInstruction.self, forKey: .primaryInstruction) - self.secondaryInstruction = try container.decodeIfPresent(VisualInstruction.self, forKey: .secondaryInstruction) - self.tertiaryInstruction = try container.decodeIfPresent(VisualInstruction.self, forKey: .tertiaryInstruction) - self.quaternaryInstruction = try container.decodeIfPresent( - VisualInstruction.self, - forKey: .quaternaryInstruction - ) - if let directlyEncoded = try container.decodeIfPresent(DrivingSide.self, forKey: .drivingSide) { - self.drivingSide = directlyEncoded - } else { - self.drivingSide = .default - } - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - // MARK: Timing When to Display the Banner - - /// The distance at which the visual instruction should be shown, measured in meters from the beginning of the step. - public let distanceAlongStep: LocationDistance - - // MARK: Getting the Instructions to Display - - /// The most important information to convey to the user about the ``RouteStep``. - public let primaryInstruction: VisualInstruction - - /// Less important details about the ``RouteStep``. - public let secondaryInstruction: VisualInstruction? - - /// A visual instruction that is presented simultaneously to provide information about an additional maneuver that - /// occurs in rapid succession. - /// - /// This instruction could either contain the visual layout information or the lane information about the upcoming - /// maneuver. - public let tertiaryInstruction: VisualInstruction? - - /// A visual instruction that is presented to provide information about the incoming junction. - /// - /// This instruction displays a zoomed image of incoming junction. - public let quaternaryInstruction: VisualInstruction? - - // MARK: Respecting Regional Driving Rules - - /// Which side of a bidirectional road the driver should drive on, also known as the rule of the road. - public var drivingSide: DrivingSide -} - -extension VisualInstructionBanner { - public static func == (lhs: VisualInstructionBanner, rhs: VisualInstructionBanner) -> Bool { - return lhs.distanceAlongStep == rhs.distanceAlongStep && - lhs.primaryInstruction == rhs.primaryInstruction && - lhs.secondaryInstruction == rhs.secondaryInstruction && - lhs.tertiaryInstruction == rhs.tertiaryInstruction && - lhs.quaternaryInstruction == rhs.quaternaryInstruction && - lhs.drivingSide == rhs.drivingSide - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/VisualInstructionComponent.swift b/ios/Classes/Navigation/MapboxDirections/VisualInstructionComponent.swift deleted file mode 100644 index 64040f651..000000000 --- a/ios/Classes/Navigation/MapboxDirections/VisualInstructionComponent.swift +++ /dev/null @@ -1,345 +0,0 @@ -import Foundation - -#if canImport(CoreGraphics) -import CoreGraphics -#if os(macOS) -import Cocoa -#elseif os(watchOS) -import WatchKit -#else -import UIKit -#endif -#endif - -#if canImport(CoreGraphics) -/// An image scale factor. -public typealias Scale = CGFloat -#else -/// An image scale factor. -public typealias Scale = Double -#endif - -extension VisualInstruction { - /// A unit of information displayed to the user as part of a ``VisualInstruction``. - public enum Component: Equatable, Sendable { - /// The component separates two other destination components. - /// - /// If the two adjacent components are both displayed as images, you can hide this delimiter component. - case delimiter(text: TextRepresentation) - - /// The component bears the name of a place or street. - case text(text: TextRepresentation) - - /// The component is an image, such as a [route marker](https://en.wikipedia.org/wiki/Highway_shield), with a - /// fallback text representation. - /// - /// - Parameter image: The component’s preferred image representation. - /// - Parameter alternativeText: The component’s alternative text representation. Use this representation if the - /// image representation is unavailable or unusable, but consider formatting the text in a special way to - /// distinguish it from an ordinary ``VisualInstruction/Component/text(text:)`` component. - case image(image: ImageRepresentation, alternativeText: TextRepresentation) - - /// The component is an image of a zoomed junction, with a fallback text representation. - case guidanceView(image: GuidanceViewImageRepresentation, alternativeText: TextRepresentation) - - /// The component contains the localized word for “Exit”. - /// - /// This component may appear before or after an ``VisualInstruction/Component/exitCode(text:)`` component, - /// depending on the language. You can hide this component if the adjacent - /// ``VisualInstruction/Component/exitCode(text:)`` component has an obvious exit-number appearance, for example - /// with an accompanying [motorway exit - /// icon](https://commons.wikimedia.org/wiki/File:Sinnbild_Autobahnausfahrt.svg). - case exit(text: TextRepresentation) - - /// The component contains an exit number. - /// - /// You can hide the adjacent ``VisualInstruction/Component/exit(text:)`` component in favor of giving this - /// component an obvious exit-number appearance, for example by pairing it with a [motorway exit - /// icon](https://commons.wikimedia.org/wiki/File:Sinnbild_Autobahnausfahrt.svg). - case exitCode(text: TextRepresentation) - - /// A component that represents a turn lane or through lane at the approach to an intersection. - /// - /// - parameter indications: The direction or directions of travel that the lane is reserved for. - /// - parameter isUsable: Whether the user can use this lane to continue along the current route. - /// - parameter preferredDirection: Which of the `indications` is applicable to the current route when there is - /// more than one - case lane(indications: LaneIndication, isUsable: Bool, preferredDirection: ManeuverDirection?) - } -} - -extension VisualInstruction.Component { - /// A textual representation of a visual instruction component. - public struct TextRepresentation: Equatable, Sendable { - /// Initializes a text representation bearing the given abbreviatable text. - public init(text: String, abbreviation: String?, abbreviationPriority: Int?) { - self.text = text - self.abbreviation = abbreviation - self.abbreviationPriority = abbreviationPriority - } - - /// The plain text representation of this component. - public let text: String - - /// An abbreviated representation of the `text` property. - public let abbreviation: String? - - /// The priority for which the component should be abbreviated. - /// - /// A component with a lower abbreviation priority value should be abbreviated before a component with a higher - /// abbreviation priority value. - public let abbreviationPriority: Int? - } - - /// An image representation of a visual instruction component. - public struct ImageRepresentation: Equatable, Sendable { - /// File formats of visual instruction component images. - public enum Format: String, Sendable { - /// Portable Network Graphics (PNG) - case png - /// Scalable Vector Graphics (SVG) - case svg - } - - /// Initializes an image representation bearing the image at the given base URL. - public init(imageBaseURL: URL?, shield: ShieldRepresentation? = nil) { - self.imageBaseURL = imageBaseURL - self.shield = shield - } - - /// The URL whose path is the prefix of all the possible URLs returned by `imageURL(scale:format:)`. - public let imageBaseURL: URL? - - /// Optionally, a structured image representation for displaying a [highway - /// shield](https://en.wikipedia.org/wiki/Highway_shield). - public let shield: ShieldRepresentation? - - /// Returns a remote URL to the image file that represents the component. - /// - Parameters: - /// - scale: The image’s scale factor. If this argument is unspecified, the current screen’s native scale - /// factor is used. Only the values 1, 2, and 3 are currently supported. - /// - format: The file format of the image. If this argument is unspecified, PNG is used. - /// - Returns: A remote URL to the image. - public func imageURL(scale: Scale, format: Format = .png) -> URL? { - guard let imageBaseURL, - var imageURLComponents = URLComponents(url: imageBaseURL, resolvingAgainstBaseURL: false) - else { - return nil - } - imageURLComponents.path += "@\(Int(scale))x.\(format)" - return imageURLComponents.url - } - } - - /// A mapbox shield representation of a visual instruction component. - public struct ShieldRepresentation: Equatable, Codable, Sendable { - /// Initializes a mapbox shield with the given name, text color, and display ref. - public init(baseURL: URL, name: String, textColor: String, text: String) { - self.baseURL = baseURL - self.name = name - self.textColor = textColor - self.text = text - } - - /// Base URL to query the styles endpoint. - public let baseURL: URL - - /// String indicating the name of the route shield. - public let name: String - - /// String indicating the color of the text to be rendered on the route shield. - public let textColor: String - - /// String indicating the route reference code that will be displayed on the shield. - public let text: String - - private enum CodingKeys: String, CodingKey { - case baseURL = "base_url" - case name - case textColor = "text_color" - case text = "display_ref" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.baseURL = try container.decode(URL.self, forKey: .baseURL) - self.name = try container.decode(String.self, forKey: .name) - self.textColor = try container.decode(String.self, forKey: .textColor) - self.text = try container.decode(String.self, forKey: .text) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(baseURL, forKey: .baseURL) - try container.encode(name, forKey: .name) - try container.encode(textColor, forKey: .textColor) - try container.encode(text, forKey: .text) - } - } -} - -/// A guidance view image representation of a visual instruction component. -public struct GuidanceViewImageRepresentation: Equatable, Sendable { - /// Initializes an image representation bearing the image at the given URL. - public init(imageURL: URL?) { - self.imageURL = imageURL - } - - /// Returns a remote URL to the image file that represents the component. - public let imageURL: URL? -} - -extension VisualInstruction.Component: Codable { - private enum CodingKeys: String, CodingKey { - case kind = "type" - case text - case abbreviatedText = "abbr" - case abbreviatedTextPriority = "abbr_priority" - case imageBaseURL - case imageURL - case shield = "mapbox_shield" - case directions - case isActive = "active" - case activeDirection = "active_direction" - } - - enum Kind: String, Codable, Sendable { - case delimiter - case text - case image = "icon" - case guidanceView = "guidance-view" - case exit - case exitCode = "exit-number" - case lane - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let kind = (try? container.decode(Kind.self, forKey: .kind)) ?? .text - - if kind == .lane { - let indications = try container.decode(LaneIndication.self, forKey: .directions) - let isUsable = try container.decode(Bool.self, forKey: .isActive) - let preferredDirection = try container.decodeIfPresent(ManeuverDirection.self, forKey: .activeDirection) - self = .lane(indications: indications, isUsable: isUsable, preferredDirection: preferredDirection) - return - } - - let text = try container.decode(String.self, forKey: .text) - let abbreviation = try container.decodeIfPresent(String.self, forKey: .abbreviatedText) - let abbreviationPriority = try container.decodeIfPresent(Int.self, forKey: .abbreviatedTextPriority) - let textRepresentation = TextRepresentation( - text: text, - abbreviation: abbreviation, - abbreviationPriority: abbreviationPriority - ) - - switch kind { - case .delimiter: - self = .delimiter(text: textRepresentation) - case .text: - self = .text(text: textRepresentation) - case .image: - var imageBaseURL: URL? - if let imageBaseURLString = try container.decodeIfPresent(String.self, forKey: .imageBaseURL) { - imageBaseURL = URL(string: imageBaseURLString) - } - let shieldRepresentation = try container.decodeIfPresent(ShieldRepresentation.self, forKey: .shield) - let imageRepresentation = ImageRepresentation(imageBaseURL: imageBaseURL, shield: shieldRepresentation) - self = .image(image: imageRepresentation, alternativeText: textRepresentation) - case .exit: - self = .exit(text: textRepresentation) - case .exitCode: - self = .exitCode(text: textRepresentation) - case .lane: - preconditionFailure("Lane component should have been initialized before decoding text") - case .guidanceView: - var imageURL: URL? - if let imageURLString = try container.decodeIfPresent(String.self, forKey: .imageURL) { - imageURL = URL(string: imageURLString) - } - let guidanceViewImageRepresentation = GuidanceViewImageRepresentation(imageURL: imageURL) - self = .guidanceView(image: guidanceViewImageRepresentation, alternativeText: textRepresentation) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - let textRepresentation: TextRepresentation? - switch self { - case .delimiter(let text): - try container.encode(Kind.delimiter, forKey: .kind) - textRepresentation = text - case .text(let text): - try container.encode(Kind.text, forKey: .kind) - textRepresentation = text - case .image(let image, let alternativeText): - try container.encode(Kind.image, forKey: .kind) - textRepresentation = alternativeText - try container.encodeIfPresent(image.imageBaseURL?.absoluteString, forKey: .imageBaseURL) - try container.encodeIfPresent(image.shield, forKey: .shield) - case .exit(let text): - try container.encode(Kind.exit, forKey: .kind) - textRepresentation = text - case .exitCode(let text): - try container.encode(Kind.exitCode, forKey: .kind) - textRepresentation = text - case .lane(let indications, let isUsable, let preferredDirection): - try container.encode(Kind.lane, forKey: .kind) - textRepresentation = .init(text: "", abbreviation: nil, abbreviationPriority: nil) - try container.encode(indications, forKey: .directions) - try container.encode(isUsable, forKey: .isActive) - try container.encodeIfPresent(preferredDirection, forKey: .activeDirection) - case .guidanceView(let image, let alternativeText): - try container.encode(Kind.guidanceView, forKey: .kind) - textRepresentation = alternativeText - try container.encodeIfPresent(image.imageURL?.absoluteString, forKey: .imageURL) - } - - if let textRepresentation { - try container.encodeIfPresent(textRepresentation.text, forKey: .text) - try container.encodeIfPresent(textRepresentation.abbreviation, forKey: .abbreviatedText) - try container.encodeIfPresent(textRepresentation.abbreviationPriority, forKey: .abbreviatedTextPriority) - } - } -} - -extension VisualInstruction.Component { - public static func == (lhs: VisualInstruction.Component, rhs: VisualInstruction.Component) -> Bool { - switch (lhs, rhs) { - case (let .delimiter(lhsText), .delimiter(let rhsText)), - (let .text(lhsText), .text(let rhsText)), - (let .exit(lhsText), .exit(let rhsText)), - (let .exitCode(lhsText), .exitCode(let rhsText)): - return lhsText == rhsText - case ( - let .image(lhsURL, lhsAlternativeText), - .image(let rhsURL, let rhsAlternativeText) - ): - return lhsURL == rhsURL - && lhsAlternativeText == rhsAlternativeText - case ( - let .guidanceView(lhsURL, lhsAlternativeText), - .guidanceView(let rhsURL, let rhsAlternativeText) - ): - return lhsURL == rhsURL - && lhsAlternativeText == rhsAlternativeText - case ( - let .lane(lhsIndications, lhsIsUsable, lhsPreferredDirection), - .lane(let rhsIndications, let rhsIsUsable, let rhsPreferredDirection) - ): - return lhsIndications == rhsIndications - && lhsIsUsable == rhsIsUsable - && lhsPreferredDirection == rhsPreferredDirection - case (.delimiter, _), - (.text, _), - (.image, _), - (.exit, _), - (.exitCode, _), - (.guidanceView, _), - (.lane, _): - return false - } - } -} diff --git a/ios/Classes/Navigation/MapboxDirections/Waypoint.swift b/ios/Classes/Navigation/MapboxDirections/Waypoint.swift deleted file mode 100644 index e583a4149..000000000 --- a/ios/Classes/Navigation/MapboxDirections/Waypoint.swift +++ /dev/null @@ -1,349 +0,0 @@ -#if canImport(CoreLocation) -import CoreLocation -#endif -import Turf - -/// A ``Waypoint`` object indicates a location along a route. It may be the route’s origin or destination, or it may be -/// another location that the route visits. A waypoint object indicates the location’s geographic location along with -/// other optional information, such as a name or the user’s direction approaching the waypoint. You create a -/// ``RouteOptions`` object using waypoint objects and also receive waypoint objects in the completion handler of the -/// `Directions.calculate(_:completionHandler:)` method. -public struct Waypoint: Codable, ForeignMemberContainer, Equatable, Sendable { - public var foreignMembers: JSONObject = [:] - - private enum CodingKeys: String, CodingKey, CaseIterable { - case coordinate = "location" - case coordinateAccuracy - case targetCoordinate - case heading - case headingAccuracy - case separatesLegs - case name - case allowsArrivingOnOppositeSide - case snappedDistance = "distance" - case layer - } - - // MARK: Creating a Waypoint - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.coordinate = try container.decode(LocationCoordinate2DCodable.self, forKey: .coordinate).decodedCoordinates - - self.coordinateAccuracy = try container.decodeIfPresent(LocationAccuracy.self, forKey: .coordinateAccuracy) - - self.targetCoordinate = try container.decodeIfPresent( - LocationCoordinate2DCodable.self, - forKey: .targetCoordinate - )?.decodedCoordinates - - self.heading = try container.decodeIfPresent(LocationDirection.self, forKey: .heading) - - self.headingAccuracy = try container.decodeIfPresent(LocationDirection.self, forKey: .headingAccuracy) - - if let separates = try container.decodeIfPresent(Bool.self, forKey: .separatesLegs) { - self.separatesLegs = separates - } - - if let allows = try container.decodeIfPresent(Bool.self, forKey: .allowsArrivingOnOppositeSide) { - self.allowsArrivingOnOppositeSide = allows - } - - if let name = try container.decodeIfPresent(String.self, forKey: .name), - !name.isEmpty - { - self.name = name - } else { - self.name = nil - } - - self.snappedDistance = try container.decodeIfPresent(LocationDistance.self, forKey: .snappedDistance) - - self.layer = try container.decodeIfPresent(Int.self, forKey: .layer) - - try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(LocationCoordinate2DCodable(coordinate), forKey: .coordinate) - try container.encodeIfPresent(coordinateAccuracy, forKey: .coordinateAccuracy) - let targetCoordinateCodable = targetCoordinate != nil ? LocationCoordinate2DCodable(targetCoordinate!) : nil - try container.encodeIfPresent(targetCoordinateCodable, forKey: .targetCoordinate) - try container.encodeIfPresent(heading, forKey: .heading) - try container.encodeIfPresent(headingAccuracy, forKey: .headingAccuracy) - try container.encodeIfPresent(separatesLegs, forKey: .separatesLegs) - try container.encodeIfPresent(allowsArrivingOnOppositeSide, forKey: .allowsArrivingOnOppositeSide) - try container.encodeIfPresent(name, forKey: .name) - try container.encodeIfPresent(snappedDistance, forKey: .snappedDistance) - try container.encodeIfPresent(layer, forKey: .layer) - - try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder) - } - - /// Initializes a new waypoint object with the given geographic coordinate and an optional accuracy and name. - /// - Parameters: - /// - coordinate: The geographic coordinate of the waypoint. - /// - coordinateAccuracy: The maximum distance away from the waypoint that the route may come and still be - /// considered viable. This argument is measured in meters. A negative value means the route may be an indefinite - /// number of meters away from the route and still be considered viable. - /// It is recommended that the value of this argument be greater than the `horizontalAccuracy` property of a - /// `CLLocation` object obtained from a `CLLocationManager` object. There is a high likelihood that the user may be - /// located some distance away from a navigable road, for instance if the user is currently on a driveway or inside - /// a building. - /// - name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one - /// waypoint from another. - public init(coordinate: LocationCoordinate2D, coordinateAccuracy: LocationAccuracy? = nil, name: String? = nil) { - self.coordinate = coordinate - self.coordinateAccuracy = coordinateAccuracy - self.name = name - } - -#if canImport(CoreLocation) -#if os(tvOS) || os(watchOS) - /// Initializes a new waypoint object with the given `CLLocation` object and an optional heading value and name. - /// - /// - Note: This initializer is intended for `CLLocation` objects created using the - /// `CLLocation(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a - /// `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid - /// overfitting, since the ``Waypoint`` class’s `coordinateAccuracy` property represents the maximum allowed - /// deviation - /// from the waypoint. There is a high likelihood that the user may be located some distance away from a navigable - /// road, for instance if the user is currently on a driveway or inside a building. - /// - Parameters: - /// - location: A `CLLocation` object representing the waypoint’s location. This initializer respects the - /// `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `coordinate` and - /// `coordinateAccuracy` properties, respectively. - /// - heading: A `LocationDirection` value representing the direction from which the route must approach the - /// waypoint in order to be considered viable. This value is stored in the `headingAccuracy` property. - /// - name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one - /// waypoint from another. - public init(location: CLLocation, heading: LocationDirection? = nil, name: String? = nil) { - self.coordinate = location.coordinate - self.coordinateAccuracy = location.horizontalAccuracy - if let heading, heading >= 0 { - self.heading = heading - } - self.name = name - } -#else - /// Initializes a new waypoint object with the given `CLLocation` object and an optional `CLHeading` object and - /// name. - /// - /// - Note: This initializer is intended for `CLLocation` objects created using the - /// `CLLocation(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a - /// `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid - /// overfitting, since the ``Waypoint`` class’s ``Waypoint/coordinateAccuracy`` property represents the maximum - /// allowed deviation from the waypoint. There is a high likelihood that the user may be located some distance away - /// from a navigable road, for instance if the user is currently on a driveway of inside a building. - /// - Parameters: - /// - location: A `CLLocation` object representing the waypoint’s location. This initializer respects the - /// `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `coordinate` and - /// `coordinateAccuracy` properties, respectively. - /// - heading: A `CLHeading` object representing the direction from which the route must approach the waypoint in - /// order to be considered viable. This initializer respects the `CLHeading` class’s `trueHeading` property or - /// `magneticHeading` property, converting it into the `headingAccuracy` property. - /// - name: The name of the waypoint. This argument does not affect the route but may help you to distinguish one - /// waypoint from another. - public init(location: CLLocation, heading: CLHeading? = nil, name: String? = nil) { - self.coordinate = location.coordinate - self.coordinateAccuracy = location.horizontalAccuracy - if let heading { - self.heading = heading.trueHeading >= 0 ? heading.trueHeading : heading.magneticHeading - } - self.name = name - } -#endif -#endif - - // MARK: Positioning the Waypoint - - /// The geographic coordinate of the waypoint. - public let coordinate: LocationCoordinate2D - - /// The radius of uncertainty for the waypoint, measured in meters. - /// - /// For a route to be considered viable, it must enter this waypoint’s circle of uncertainty. The ``coordinate`` - /// property identifies the center of the circle, while this property indicates the circle’s radius. If the value of - /// this property is negative, a route is considered viable regardless of whether it enters this waypoint’s circle - /// of uncertainty, subject to an undefined maximum distance. - /// - /// By default, the value of this property is `nil`. - public var coordinateAccuracy: LocationAccuracy? - - /// The geographic coordinate of the waypoint’s target. - /// - /// The waypoint’s target affects arrival instructions without affecting the route’s shape. For example, a delivery - /// or ride hailing application may specify a waypoint target that represents a drop-off location. The target - /// determines whether the arrival visual and spoken instructions indicate that the destination is “on the left” or - /// “on the right”. - /// - /// By default, this property is set to `nil`, meaning the waypoint has no target. This property is ignored on the - /// first waypoint of a ``RouteOptions`` object, on any waypoint of a ``MatchOptions`` object, or on any waypoint of - /// a ``RouteOptions`` object if ``DirectionsOptions/includesSteps`` is set to `false`. - /// - /// This property corresponds to the - /// [`waypoint_targets`](https://docs.mapbox.com/api/navigation/#retrieve-directions) query parameter in the Mapbox - /// Directions and Map Matching APIs. - public var targetCoordinate: LocationCoordinate2D? - - /// A Boolean value indicating whether the waypoint may be snapped to a closed road in the resulting - /// ``RouteResponse``. - /// - /// If `true`, the waypoint may be snapped to a road segment that is closed due to a live traffic closure. This - /// property is `false` by default. This property corresponds to the [`snapping_include_closures`](https://docs.mapbox.com/api/navigation/directions/#optional-parameters-for-the-mapboxdriving-traffic-profile) - /// query parameter in the Mapbox Directions API. - public var allowsSnappingToClosedRoad: Bool = false - - /// A Boolean value indicating whether the waypoint may be snapped to a statically (long-term) closed road in the - /// resulting ``RouteResponse``. - /// - /// If `true`, the waypoint may be snapped to a road segment statically closed, that is long-term (for example, road - /// under construction). This property is `false` by default. This property corresponds to the [`snapping_include_static_closures`](https://docs.mapbox.com/api/navigation/directions/#optional-parameters-for-the-mapboxdriving-traffic-profile) - /// query parameter in the Mapbox Directions API. - public var allowsSnappingToStaticallyClosedRoad: Bool = false - - /// The straight-line distance from the coordinate specified in the query to the location it was snapped to in the - /// resulting ``RouteResponse``. - /// - /// By default, this property is set to `nil`, meaning the waypoint has no snapped distance. - public var snappedDistance: LocationDistance? - - /// The [layer](https://wiki.openstreetmap.org/wiki/Key:layer) of road that the waypoint is positioned which is used - /// to filter the road segment that the waypoint will be placed on in Z-order. It is useful for avoiding ambiguity - /// in the case of multi-level roads (such as a tunnel under a road). - /// - /// This property corresponds to the - /// [`layers`](https://docs.mapbox.com/api/navigation/directions/#optional-parameters) query parameter in the Mapbox - /// Directions API. If a matching layer is not found, the Mapbox Directions API will choose a suitable layer - /// according to the other provided ``DirectionsOptions`` and ``Waypoint`` properties. - /// - /// By default, this property is set to `nil`, meaning the route from the ``Waypoint`` will not be influenced by a - /// layer of road. - public var layer: Int? - - // MARK: Getting the Direction of Approach - - /// The direction from which a route must approach this waypoint in order to be considered viable. - /// - /// This property is measured in degrees clockwise from true north. A value of 0 degrees means due north, 90 degrees - /// means due east, 180 degrees means due south, and so on. If the value of this property is negative, a route is - /// considered viable regardless of the direction from which it approaches this waypoint. - /// - /// If this waypoint is the first waypoint (the source waypoint), the route must start out by heading in the - /// direction specified by this property. You should always set the ``headingAccuracy`` property in conjunction with - /// this property. If the ``headingAccuracy`` property is set to `nil`, this property is ignored. - /// - /// For driving directions, this property can be useful for avoiding a route that begins by going in the direction - /// opposite the current direction of travel. For example, if you know the user is moving eastwardly and the first - /// waypoint is the user’s current location, specifying a heading of 90 degrees and a heading accuracy of 90 degrees - /// for the first waypoint avoids a route that begins with a “head west” instruction. - /// - /// You should be certain that the user is in motion before specifying a heading and heading accuracy; otherwise, - /// you may be unnecessarily filtering out the best route. For example, suppose the user is sitting in a car parked - /// in a driveway, facing due north, with the garage in front and the street to the rear. In that case, specifying a - /// heading of 0 degrees and a heading accuracy of 90 degrees may result in a route that begins on the back alley - /// or, worse, no route at all. For this reason, it is recommended that you only specify a heading and heading - /// accuracy when automatically recalculating directions due to the user deviating from the route. - /// - /// By default, the value of this property is `nil`, meaning that a route is considered viable regardless of the - /// direction of approach. - public var heading: LocationDirection? = nil - - /// The maximum amount, in degrees, by which a route’s approach to a waypoint may differ from ``heading`` in either - /// direction in order to be considered viable. - /// - /// A value of 0 degrees means that the approach must match the specified ``heading`` exactly – an unlikely - /// scenario. A value of 180 degrees or more means that the approach may be as much as 180 degrees in either - /// direction from the specified ``heading``, effectively allowing a candidate route to approach the waypoint from - /// any direction. - /// - /// If you set the ``heading`` property, you should set this property to a value such as 90 degrees, to avoid - /// filtering out routes whose approaches differ only slightly from the specified `heading`. Otherwise, if the - /// ``heading`` property is set to a negative value, this property is ignored. - /// - /// By default, the value of this property is `nil`, meaning that a route is considered viable regardless of the - /// direction of approach. - public var headingAccuracy: LocationDirection? = nil - - var headingDescription: String { - guard let heading, heading >= 0, - let accuracy = headingAccuracy, accuracy >= 0 - else { - return "" - } - - return "\(heading.truncatingRemainder(dividingBy: 360)),\(min(accuracy, 180))" - } - - /// A Boolean value indicating whether arriving on opposite side is allowed. - /// - /// This property has no effect if ``DirectionsOptions/includesSteps`` is set to `false`. - /// This property corresponds to the - /// [`approaches`](https://www.mapbox.com/api-documentation/navigation/#retrieve-directions) query parameter in the - /// Mapbox Directions and Map Matching APIs. - public var allowsArrivingOnOppositeSide = true - - // MARK: Identifying the Waypoint - - /// The name of the waypoint. - /// - /// This property does not affect the route, but the name is included in the arrival instruction, to help the user - /// distinguish between multiple destinations. The name can also help you distinguish one waypoint from another in - /// the array of waypoints passed into the completion handler of the `Directions.calculate(_:completionHandler:)` - /// method. - public var name: String? - - // MARK: Separating the Routes Into Legs - - /// A Boolean value indicating whether the waypoint is significant enough to appear in the resulting routes as a - /// waypoint separating two legs, along with corresponding guidance instructions. - /// - /// By default, this property is set to `true`, which means that each resulting route will include a leg that ends - /// by arriving at the waypoint as ``RouteLeg/destination`` and a subsequent leg that begins by departing from the - /// waypoint as ``RouteLeg/source``. Otherwise, if this property is set to `false`, a single leg passes through the - /// waypoint without specifically mentioning it. Regardless of the value of this property, each resulting route - /// passes through the location specified by the ``coordinate`` property, accounting for approach-related properties - /// such as ``heading``. - /// - /// With the Mapbox Directions API, set this property to `false` if you want the waypoint’s location to influence - /// the path that the route follows without attaching any meaning to the waypoint object itself. With the Mapbox Map - /// Matching API, use this property when the ``DirectionsOptions/includesSteps`` property is `true` or when - /// ``coordinate`` represents a trace with a high sample rate. - /// - /// This property has no effect if ``DirectionsOptions/includesSteps`` is set to `false`, or if - /// ``MatchOptions/waypointIndices`` is non-nil. - /// This property corresponds to the [`approaches`](https://docs.mapbox.com/api/navigation/#retrieve-directions) - /// query parameter in the Mapbox Directions and Map Matching APIs. - public var separatesLegs: Bool = true -} - -extension Waypoint: CustomStringConvertible { - public var description: String { - return Mirror(reflecting: self).children.compactMap { - if let label = $0.label { - return "\(label): \($0.value)" - } - - return "" - }.joined(separator: "\n") - } -} - -#if canImport(CoreLocation) -extension Waypoint: CustomQuickLookConvertible { - func debugQuickLookObject() -> Any? { - return CLLocation( - coordinate: targetCoordinate ?? coordinate, - altitude: 0, - horizontalAccuracy: coordinateAccuracy ?? -1, - verticalAccuracy: -1, - course: heading ?? -1, - speed: -1, - timestamp: Date() - ) - } -} -#endif diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Billing/ApiConfiguration.swift b/ios/Classes/Navigation/MapboxNavigationCore/Billing/ApiConfiguration.swift deleted file mode 100644 index 61a33caa5..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Billing/ApiConfiguration.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation -import MapboxDirections - -/// The Mapbox access token specified in the main application bundle’s Info.plist. -private let defaultAccessToken: String? = - Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String ?? - Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAccessToken") as? String ?? - UserDefaults.standard.string(forKey: "MBXAccessToken") - -/// Configures access token for Mapbox API requests. -public struct ApiConfiguration: Sendable, Equatable { - /// The default configuration. The SDK will attempt to find an access token from your app's `Info.plist`. - public static var `default`: Self { - guard let defaultAccessToken, !defaultAccessToken.isEmpty else { - preconditionFailure( - "A Mapbox access token is required. Go to . In Info.plist, set the MBXAccessToken key to your access token." - ) - } - - return .init(accessToken: defaultAccessToken, endPoint: .mapboxApiEndpoint()) - } - - /// A Mapbox [access token](https://www.mapbox.com/help/define-access-token/) used to authorize Mapbox API requests. - public let accessToken: String - /// An optional hostname to the server API. Defaults to `api.mapbox.com`. - @_spi(MapboxInternal) - public let endPoint: URL - - /// Initializes ``ApiConfiguration`` instance. - /// - Parameters: - /// - accessToken: A Mapbox [access token](https://www.mapbox.com/help/define-access-token/) used to authorize - /// Mapbox API requests. - public init(accessToken: String) { - self.init(accessToken: accessToken, endPoint: nil) - } - - /// Initializes ``ApiConfiguration`` instance. - /// - Parameters: - /// - accessToken: A Mapbox [access token](https://www.mapbox.com/help/define-access-token/) used to authorize - /// Mapbox API requests. - /// - endPoint: An optional hostname to the server API. - @_spi(MapboxInternal) - public init( - accessToken: String, - endPoint: URL? - ) { - self.accessToken = accessToken - self.endPoint = endPoint ?? .mapboxApiEndpoint() - } - - init(requestURL url: URL) { - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) - let accessToken = components? - .queryItems? - .first { $0.name == .accessTokenUrlQueryItemName }? - .value - components?.path = "" - components?.queryItems = nil - self.init( - accessToken: accessToken ?? defaultAccessToken!, - endPoint: components?.url ?? .mapboxApiEndpoint() - ) - } - - func accessTokenUrlQueryItem() -> URLQueryItem { - .init(name: .accessTokenUrlQueryItemName, value: accessToken) - } -} - -extension Credentials { - init(_ apiConfiguration: ApiConfiguration) { - self.init(accessToken: apiConfiguration.accessToken, host: apiConfiguration.endPoint.absoluteURL) - } -} - -extension String { - fileprivate static let accessTokenUrlQueryItemName: String = "access_token" -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler+SkuTokenProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler+SkuTokenProvider.swift deleted file mode 100644 index 55d863f62..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler+SkuTokenProvider.swift +++ /dev/null @@ -1,9 +0,0 @@ -import _MapboxNavigationHelpers - -extension BillingHandler { - func skuTokenProvider() -> SkuTokenProvider { - .init { - self.serviceSkuToken - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler.swift b/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler.swift deleted file mode 100644 index 9fee5fc86..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Billing/BillingHandler.swift +++ /dev/null @@ -1,477 +0,0 @@ -// IMPORTANT: Tampering with any file that contains billing code is a violation of our ToS -// and will result in enforcement of the penalties stipulated in the ToS. - -import Foundation -import MapboxCommon_Private -import MapboxDirections - -/// Wrapper around `MapboxCommon_Private.BillingServiceFactory`, which provides its shared instance. -enum NativeBillingService { - /// Provides a new or an existing `MapboxCommon`s `BillingServiceFactory` instance. - static var shared: MapboxCommon_Private.BillingService { - MapboxCommon_Private.BillingServiceFactory.getInstance() - } -} - -/// BillingServiceError from MapboxCommon -private typealias BillingServiceErrorNative = MapboxCommon_Private.BillingServiceError - -/// Swift variant of `BillingServiceErrorNative` -enum BillingServiceError: Error { - /// Unknown error from Billing Service - case unknown - /// The request failed because the access token is invalid. - case tokenValidationFailed - /// The resume failed because the session doesn't exist or invalid. - case resumeFailed - - fileprivate init(_ nativeError: BillingServiceErrorNative) { - switch nativeError.code { - case .resumeFailed: - self = .resumeFailed - case .tokenValidationFailed: - self = .tokenValidationFailed - @unknown default: - self = .unknown - } - } -} - -/// Protocol for `NativeBillingService` implementation. Inversing the dependency on `NativeBillingService` allows us -/// to unit test our implementation. -protocol BillingService: Sendable { - func getSKUTokenIfValid(for sessionType: BillingHandler.SessionType) -> String - func beginBillingSession( - for sessionType: BillingHandler.SessionType, - onError: @escaping (BillingServiceError) -> Void - ) - func pauseBillingSession(for sessionType: BillingHandler.SessionType) - func resumeBillingSession( - for sessionType: BillingHandler.SessionType, - onError: @escaping (BillingServiceError) -> Void - ) - func stopBillingSession(for sessionType: BillingHandler.SessionType) - func triggerBillingEvent(onError: @escaping (BillingServiceError) -> Void) - func getSessionStatus(for sessionType: BillingHandler.SessionType) -> BillingHandler.SessionState -} - -/// Implementation of `BillingService` protocol which uses `NativeBillingService`. -private final class ProductionBillingService: BillingService { - /// `UserSKUIdentifier` which is used for navigation MAU billing events. - private let mauSku: UserSKUIdentifier = .nav3CoreMAU - private var sdkInformation: SdkInformation { - .init( - name: SdkInfo.navigationUX.name, - version: SdkInfo.navigationUX.version, - packageName: SdkInfo.navigationUX.packageName - ) - } - - init() {} - - func getSKUTokenIfValid(for sessionType: BillingHandler.SessionType) -> String { - NativeBillingService.shared.getSessionSKUTokenIfValid(for: tripSku(for: sessionType)) - } - - func beginBillingSession( - for sessionType: BillingHandler.SessionType, - onError: @escaping (BillingServiceError) -> Void - ) { - let skuToken = tripSku(for: sessionType) - Log.info("\(sessionType) billing session starts", category: .billing) - - NativeBillingService.shared.beginBillingSession( - for: sdkInformation, - skuIdentifier: skuToken, - callback: { - nativeBillingServiceError in - onError(BillingServiceError(nativeBillingServiceError)) - }, - validity: sessionType.maxSessionInterval - ) - } - - func pauseBillingSession(for sessionType: BillingHandler.SessionType) { - let skuToken = tripSku(for: sessionType) - Log.info("\(sessionType) billing session pauses", category: .billing) - NativeBillingService.shared.pauseBillingSession(for: skuToken) - } - - func resumeBillingSession( - for sessionType: BillingHandler.SessionType, - onError: @escaping (BillingServiceError) -> Void - ) { - let skuToken = tripSku(for: sessionType) - Log.info("\(sessionType) billing session resumes", category: .billing) - NativeBillingService.shared.resumeBillingSession(for: skuToken) { nativeBillingServiceError in - onError(BillingServiceError(nativeBillingServiceError)) - } - } - - func stopBillingSession(for sessionType: BillingHandler.SessionType) { - let skuToken = tripSku(for: sessionType) - Log.info("\(sessionType) billing session stops", category: .billing) - NativeBillingService.shared.stopBillingSession(for: skuToken) - } - - func triggerBillingEvent(onError: @escaping (BillingServiceError) -> Void) { - NativeBillingService.shared.triggerUserBillingEvent( - for: sdkInformation, - skuIdentifier: mauSku - ) { nativeBillingServiceError in - onError(BillingServiceError(nativeBillingServiceError)) - } - } - - func getSessionStatus(for sessionType: BillingHandler.SessionType) -> BillingHandler.SessionState { - switch NativeBillingService.shared.getSessionStatus(for: tripSku(for: sessionType)) { - case .noSession: return .stopped - case .sessionActive: return .running - case .sessionPaused: return .paused - @unknown default: - preconditionFailure("Unsupported session status from NativeBillingService.") - } - } - - private func tripSku(for sessionType: BillingHandler.SessionType) -> SessionSKUIdentifier { - switch sessionType { - case .activeGuidance: - return .nav3SesCoreAGTrip - case .freeDrive: - return .nav3SesCoreFDTrip - } - } -} - -/// Receives events about navigation changes and triggers appropriate events in `BillingService`. -/// -/// Session can be paused (`BillingHandler.pauseBillingSession(with:)`), stopped -/// (`BillingHandler.stopBillingSession(with:)`) or resumed (`BillingHandler.resumeBillingSession(with:)`). -/// -/// State of the billing sessions can be obtained using `BillingHandler.sessionState(uuid:)`. -final class BillingHandler: @unchecked Sendable { - /// Parameters on an active session. - private struct Session { - let type: SessionType - /// Indicates whether the session is active but paused. - var isPaused: Bool - } - - /// The state of the billing session. - enum SessionState: Equatable { - /// Indicates that there is no active billing session. - case stopped - /// There is an active paused billing session. - case paused - /// There is an active running billing session. - case running - } - - /// Supported session types. - enum SessionType: Equatable, CustomStringConvertible { - case freeDrive - case activeGuidance - - var maxSessionInterval: TimeInterval { - switch self { - case .activeGuidance: - return 43200 /* 12h */ - case .freeDrive: - return 3600 /* 1h */ - } - } - - var description: String { - switch self { - case .activeGuidance: - return "Active Guidance" - case .freeDrive: - return "Free Drive" - } - } - } - - static func createInstance(with accessToken: String?) -> BillingHandler { - precondition( - accessToken != nil, - "A Mapbox access token is required. Go to . In Info.plist, set the MBXAccessToken key to your access token." - ) - let service = ProductionBillingService() - return .init(service: service) - } - - /// The billing service which is used to send billing events. - private let billingService: BillingService - - /// A lock which serializes access to variables with underscore: `_sessions` etc. - /// As a convention, all class-level identifiers that starts with `_` should be executed with locked `lock`. - private let lock: NSLock = .init() - - /// All currently active sessions. Running or paused. When session is stopped, it is removed from this variable. - /// These sessions are different from `NativeBillingService` sessions. `BillingHandler.Session`s are mapped to one - /// `NativeBillingService`'s session for each `BillingHandler.SessionType`. - private var _sessions: [UUID: Session] = [:] - - /// The state of the billing session. - /// - /// - Important: This variable is safe to use from any thread. - /// - Parameter uuid: Session UUID which is provided in `BillingHandler.beginBillingSession(for:uuid:)`. - func sessionState(uuid: UUID) -> SessionState { - lock.lock(); defer { - lock.unlock() - } - - guard let session = _sessions[uuid] else { - return .stopped - } - - if session.isPaused { - return .paused - } else { - return .running - } - } - - func sessionType(uuid: UUID) -> SessionType? { - lock.lock(); defer { - lock.unlock() - } - - guard let session = _sessions[uuid] else { - return nil - } - return session.type - } - - /// The token to use for service requests like `Directions` etc. - var serviceSkuToken: String { - let sessionTypes: [BillingHandler.SessionType] = [.activeGuidance, .freeDrive] - - for sessionType in sessionTypes { - switch billingService.getSessionStatus(for: sessionType) { - case .running: - return billingService.getSKUTokenIfValid(for: sessionType) - case .paused, .stopped: - continue - } - } - - return "" - } - - private init(service: BillingService) { - self.billingService = service - } - - /// Starts a new billing session of the given `sessionType` identified by `uuid`. - /// - /// The `uuid` that is used to create a billing session must be provided in the following methods to perform - /// relevant changes to the started billing session: - /// - `BillingHandler.stopBillingSession(with:)` - /// - `BillingHandler.pauseBillingSession(with:)` - /// - `BillingHandler.resumeBillingSession(with:)` - /// - /// - Parameters: - /// - sessionType: The type of the billing session. - /// - uuid: The unique identifier of the billing session. - func beginBillingSession(for sessionType: SessionType, uuid: UUID) { - lock.lock() - - if var existingSession = _sessions[uuid] { - existingSession.isPaused = false - _sessions[uuid] = existingSession - } else { - let session = Session(type: sessionType, isPaused: false) - _sessions[uuid] = session - } - - let sessionStatus = billingService.getSessionStatus(for: sessionType) - - lock.unlock() - - switch sessionStatus { - case .stopped: - billingService.triggerBillingEvent(onError: { _ in - Log.fault("MAU isn't counted", category: .billing) - }) - billingService.beginBillingSession(for: sessionType, onError: { [weak self] error in - Log.fault( - "Trip session isn't started. Please check that you have the correct Mapboox Access Token", - category: .billing - ) - - switch error { - case .tokenValidationFailed: - assertionFailure( - "Token validation failed. Please check that you have the correct Mapbox Access Token." - ) - case .resumeFailed, .unknown: - break - } - self?.failedToBeginBillingSession(with: uuid, with: error) - }) - case .paused: - resumeBillingSession(with: uuid) - case .running: - break - } - } - - /// Starts a new billing session in `billingService` if a session with `uuid` exists. - /// - /// Use this method to force `billingService` to start a new billing session. - func beginNewBillingSessionIfExists(with uuid: UUID) { - lock.lock() - - guard let session = _sessions[uuid] else { - lock.unlock(); return - } - - lock.unlock() - - billingService.beginBillingSession(for: session.type) { error in - Log.fault( - "New trip session isn't started. Please check that you have the correct Mapboox Access Token.", - category: .billing - ) - - switch error { - case .tokenValidationFailed: - assertionFailure( - "Token validation failed. Please check that you have the correct Mapboox Access Token." - ) - case .resumeFailed, .unknown: - break - } - } - - if session.isPaused { - pauseBillingSession(with: uuid) - } - } - - /// Stops the billing session identified by the `uuid`. - func stopBillingSession(with uuid: UUID) { - lock.lock() - guard let session = _sessions[uuid] else { - lock.unlock(); return - } - _sessions[uuid] = nil - - let hasSessionWithSameType = _hasSession(with: session.type) - let triggerStopSessionEvent = !hasSessionWithSameType - && billingService.getSessionStatus(for: session.type) != .stopped - let triggerPauseSessionEvent = - !triggerStopSessionEvent - && hasSessionWithSameType - && !_hasSession(with: session.type, isPaused: false) - && billingService.getSessionStatus(for: session.type) != .paused - lock.unlock() - - if triggerStopSessionEvent { - billingService.stopBillingSession(for: session.type) - } else if triggerPauseSessionEvent { - billingService.pauseBillingSession(for: session.type) - } - } - - /// Pauses the billing session identified by the `uuid`. - func pauseBillingSession(with uuid: UUID) { - lock.lock() - guard var session = _sessions[uuid] else { - assertionFailure("Trying to pause non-existing session.") - lock.unlock(); return - } - session.isPaused = true - _sessions[uuid] = session - - let triggerBillingServiceEvent = !_hasSession(with: session.type, isPaused: false) - && billingService.getSessionStatus(for: session.type) == .running - lock.unlock() - - if triggerBillingServiceEvent { - billingService.pauseBillingSession(for: session.type) - } - } - - /// Resumes the billing session identified by the `uuid`. - func resumeBillingSession(with uuid: UUID) { - lock.lock() - guard var session = _sessions[uuid] else { - assertionFailure("Trying to resume non-existing session.") - lock.unlock(); return - } - session.isPaused = false - _sessions[uuid] = session - let triggerBillingServiceEvent = billingService.getSessionStatus(for: session.type) == .paused - lock.unlock() - - if triggerBillingServiceEvent { - billingService.resumeBillingSession(for: session.type) { _ in - self.failedToResumeBillingSession(with: uuid) - } - } - } - - func shouldStartNewBillingSession(for newRoute: Route, remainingWaypoints: [Waypoint]) -> Bool { - let newRouteWaypoints = newRoute.legs.compactMap(\.destination) - - guard !newRouteWaypoints.isEmpty else { - return false // Don't need to bil for routes without waypoints - } - - guard newRouteWaypoints.count == remainingWaypoints.count else { - Log.info( - "A new route is about to be set with a different set of waypoints, leading to the initiation of a new Active Guidance trip. For more information, see the “[Pricing](https://docs.mapbox.com/ios/beta/navigation/guides/pricing/)” guide.", - category: .billing - ) - return true - } - - for (newWaypoint, currentWaypoint) in zip(newRouteWaypoints, remainingWaypoints) { - if newWaypoint.coordinate.distance(to: currentWaypoint.coordinate) > 100 { - Log.info( - "A new route waypoint \(newWaypoint) is further than 100 meters from current waypoint \(currentWaypoint), leading to the initiation of a new Active Guidance trip. For more information, see the “[Pricing](https://docs.mapbox.com/ios/navigation/guides/pricing/)” guide. ", - category: .billing - ) - return true - } - } - - return false - } - - private func failedToBeginBillingSession(with uuid: UUID, with error: Error) { - lock { - _sessions[uuid] = nil - } - } - - private func failedToResumeBillingSession(with uuid: UUID) { - lock.lock() - guard let session = _sessions[uuid] else { - lock.unlock(); return - } - _sessions[uuid] = nil - lock.unlock() - beginBillingSession(for: session.type, uuid: uuid) - } - - private func _hasSession(with type: SessionType) -> Bool { - return _sessions.contains(where: { $0.value.type == type }) - } - - private func _hasSession(with type: SessionType, isPaused: Bool) -> Bool { - return _sessions.values.contains { session in - session.type == type && session.isPaused == isPaused - } - } -} - -// MARK: - Tests Support - -extension BillingHandler { - static func __createMockedHandler(with service: BillingService) -> BillingHandler { - BillingHandler(service: service) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Billing/SkuTokenProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Billing/SkuTokenProvider.swift deleted file mode 100644 index 60a24d578..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Billing/SkuTokenProvider.swift +++ /dev/null @@ -1,7 +0,0 @@ -public struct SkuTokenProvider: Sendable { - public let skuToken: @Sendable () -> String? - - public init(skuToken: @Sendable @escaping () -> String?) { - self.skuToken = skuToken - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Cache/FileCache.swift b/ios/Classes/Navigation/MapboxNavigationCore/Cache/FileCache.swift deleted file mode 100644 index 3cefc84fd..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Cache/FileCache.swift +++ /dev/null @@ -1,92 +0,0 @@ -import Foundation - -final class FileCache: Sendable { - typealias CompletionHandler = @Sendable () -> Void - - let diskCacheURL: URL = { - let fileManager = FileManager.default - let basePath = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first! - let identifier = Bundle.mapboxNavigationUXCore.bundleIdentifier! - return basePath.appendingPathComponent(identifier + ".downloadedFiles") - }() - - let diskAccessQueue = DispatchQueue(label: Bundle.mapboxNavigationUXCore.bundleIdentifier! + ".diskAccess") - - /// Stores data in the file cache for the given key, and calls the completion handler when finished. - public func store(_ data: Data, forKey key: String, completion: CompletionHandler? = nil) { - diskAccessQueue.async { - self.createCacheDirIfNeeded(self.diskCacheURL) - let cacheURL = self.cacheURLWithKey(key) - - do { - try data.write(to: cacheURL) - } catch { - Log.error( - "Failed to write data to URL \(cacheURL)", - category: .navigationUI - ) - } - completion?() - } - } - - /// Returns data from the file cache for the given key - public func data(forKey key: String) -> Data? { - let cacheKey = cacheURLWithKey(key) - do { - return try diskAccessQueue.sync { - try Data(contentsOf: cacheKey) - } - } catch { - return nil - } - } - - /// Clears the disk cache by removing and recreating the cache directory, and calls the completion handler when - /// finished. - public func clearDisk(completion: CompletionHandler? = nil) { - let cacheURL = diskCacheURL - diskAccessQueue.async { - do { - let fileManager = FileManager() - try fileManager.removeItem(at: cacheURL) - } catch { - Log.error( - "Failed to remove cache dir: \(cacheURL)", - category: .navigationUI - ) - } - - self.createCacheDirIfNeeded(cacheURL) - - completion?() - } - } - - private func cacheURLWithKey(_ key: String) -> URL { - let cacheKey = cacheKeyForKey(key) - return diskCacheURL.appendingPathComponent(cacheKey) - } - - private func cacheKeyForKey(_ key: String) -> String { - key.sha256 - } - - private func createCacheDirIfNeeded(_ url: URL) { - let fileManager = FileManager() - if fileManager.fileExists(atPath: url.absoluteString) == false { - do { - try fileManager.createDirectory( - at: url, - withIntermediateDirectories: true, - attributes: nil - ) - } catch { - Log.error( - "Failed to create directory: \(url)", - category: .navigationUI - ) - } - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Cache/SyncBimodalCache.swift b/ios/Classes/Navigation/MapboxNavigationCore/Cache/SyncBimodalCache.swift deleted file mode 100644 index bfbdb6e18..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Cache/SyncBimodalCache.swift +++ /dev/null @@ -1,85 +0,0 @@ -import Foundation -import UIKit - -protocol SyncBimodalCache { - func clear(mode: CacheMode) - func store(data: Data, key: String, mode: CacheMode) - - subscript(key: String) -> Data? { get } -} - -struct CacheMode: OptionSet { - var rawValue: Int - - init(rawValue: Int) { - self.rawValue = rawValue - } - - static let InMemory = CacheMode(rawValue: 1 << 0) - static let OnDisk = CacheMode(rawValue: 1 << 1) -} - -final class MapboxSyncBimodalCache: SyncBimodalCache, @unchecked Sendable { - private let accessLock: NSLock - private let memoryCache: NSCache - private let fileCache = FileCache() - - public init() { - self.accessLock = .init() - self.memoryCache = NSCache() - memoryCache.name = "In-Memory Data Cache" - - DispatchQueue.main.async { - NotificationCenter.default.addObserver( - self, - selector: #selector(self.clearMemory), - name: UIApplication.didReceiveMemoryWarningNotification, - object: nil - ) - } - } - - @objc - func clearMemory() { - accessLock.withLock { - memoryCache.removeAllObjects() - } - } - - func clear(mode: CacheMode) { - accessLock.withLock { - if mode.contains(.InMemory) { - memoryCache.removeAllObjects() - } else if mode.contains(.OnDisk) { - fileCache.clearDisk() - } - } - } - - func store(data: Data, key: String, mode: CacheMode) { - accessLock.withLock { - if mode.contains(.InMemory) { - memoryCache.setObject( - data as NSData, - forKey: key as NSString - ) - } else if mode.contains(.OnDisk) { - fileCache.store( - data, - forKey: key - ) - } - } - } - - subscript(key: String) -> Data? { - accessLock.withLock { - return memoryCache.object( - forKey: key as NSString - ) as Data? ?? - fileCache.data( - forKey: key - ) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/CoreConstants.swift b/ios/Classes/Navigation/MapboxNavigationCore/CoreConstants.swift deleted file mode 100644 index 0ef99dd68..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/CoreConstants.swift +++ /dev/null @@ -1,190 +0,0 @@ -import Foundation - -extension Notification.Name { - // MARK: Switching Navigation Tile Versions - - /// Posted when Navigator has not enough tiles for map matching on current tiles version, but there are suitable - /// older versions inside underlying Offline Regions. Navigator has restarted when this notification is issued. - /// - /// Such action invalidates all existing matched ``RoadObject`` which should be re-applied manually. - /// The user info dictionary contains the key ``Navigator/NotificationUserInfoKey/tilesVersionKey`` - @_documentation(visibility: internal) - public static let navigationDidSwitchToFallbackVersion: Notification - .Name = .init(rawValue: "NavigatorDidFallbackToOfflineVersion") - - /// Posted when Navigator was switched to a fallback offline tiles version, but latest tiles became available again. - /// Navigator has restarted when this notification is issued. - /// Such action invalidates all existing matched ``RoadObject``s which should be re-applied manually. - /// The user info dictionary contains the key ``NativeNavigator/NotificationUserInfoKey/tilesVersionKey`` - @_documentation(visibility: internal) - public static let navigationDidSwitchToTargetVersion: Notification - .Name = .init(rawValue: "NavigatorDidRestoreToOnlineVersion") - - /// Posted when NavNative sends updated navigation status. - /// - /// The user info dictionary contains the keys ``Navigator.NotificationUserInfoKey.originKey`` and - /// ``Navigator/NotificationUserInfoKey/statusKey``. - static let navigationStatusDidChange: Notification.Name = .init(rawValue: "NavigationStatusDidChange") -} - -extension Notification.Name { - // MARK: Handling Alternative Routes - - static let navigatorDidChangeAlternativeRoutes: Notification - .Name = .init(rawValue: "NavigatorDidChangeAlternativeRoutes") - - static let navigatorDidFailToChangeAlternativeRoutes: Notification - .Name = .init(rawValue: "NavigatorDidFailToChangeAlternativeRoutes") - - static let navigatorWantsSwitchToCoincideOnlineRoute: Notification - .Name = .init(rawValue: "NavigatorWantsSwitchToCoincideOnlineRoute") -} - -extension Notification.Name { - // MARK: Electronic Horizon Notifications - - /// Posted when the user’s position in the electronic horizon changes. This notification may be posted multiple - /// times after ``Foundation/NSNotification/Name/electronicHorizonDidEnterRoadObject`` until the user transitions to - /// a new electronic horizon. - /// - /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/positionKey``, - /// ``RoadGraph/NotificationUserInfoKey/treeKey``, ``RoadGraph/NotificationUserInfoKey/updatesMostProbablePathKey``, - /// and ``RoadGraph/NotificationUserInfoKey/distancesByRoadObjectKey``. - public static let electronicHorizonDidUpdatePosition: Notification.Name = - .init(rawValue: "ElectronicHorizonDidUpdatePosition") - - /// Posted when the user enters a linear road object. - /// - /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/roadObjectIdentifierKey`` and - /// ``RoadGraph/NotificationUserInfoKey/didTransitionAtEndpointKey``. - public static let electronicHorizonDidEnterRoadObject: Notification.Name = - .init(rawValue: "ElectronicHorizonDidEnterRoadObject") - - /// Posted when the user exits a linear road object. - /// - /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/roadObjectIdentifierKey`` and - /// ``RoadGraph/NotificationUserInfoKey/didTransitionAtEndpointKey``. - public static let electronicHorizonDidExitRoadObject: Notification.Name = - .init(rawValue: "ElectronicHorizonDidExitRoadObject") - - /// Posted when user has passed point-like object. - /// - /// The user info dictionary contains the key ``RoadGraph/NotificationUserInfoKey/roadObjectIdentifierKey``. - public static let electronicHorizonDidPassRoadObject: Notification.Name = - .init(rawValue: "ElectronicHorizonDidPassRoadObject") -} - -extension Notification.Name { - // MARK: Route Refreshing Notifications - - /// Posted when the user’s position in the electronic horizon changes. This notification may be posted multiple - /// times after ``electronicHorizonDidEnterRoadObject`` until the user transitions to a new electronic horizon. - /// - /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/positionKey``, - /// ``RoadGraph/NotificationUserInfoKey/treeKey``, ``RoadGraph/NotificationUserInfoKey/updatesMostProbablePathKey``, - /// and ``RoadGraph/NotificationUserInfoKey/distancesByRoadObjectKey``. - static let routeRefreshDidUpdateAnnotations: Notification.Name = .init(rawValue: "RouteRefreshDidUpdateAnnotations") - - /// Posted when the user enters a linear road object. - /// - /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/roadObjectIdentifierKey`` and - /// ``RoadGraph/NotificationUserInfoKey/didTransitionAtEndpointKey``. - static let routeRefreshDidCancelRefresh: Notification.Name = .init(rawValue: "RouteRefreshDidCancelRefresh") - - /// Posted when the user exits a linear road object. - /// - /// The user info dictionary contains the keys ``RoadGraph/NotificationUserInfoKey/roadObjectIdentifierKey`` and - /// ``RoadGraph.NotificationUserInfoKey.transitionKey``. - static let routeRefreshDidFailRefresh: Notification.Name = .init(rawValue: "RouteRefreshDidFailRefresh") -} - -extension NativeNavigator { - /// Keys in the user info dictionaries of various notifications posted by instances of `NativeNavigator`. - public struct NotificationUserInfoKey: Hashable, Equatable, RawRepresentable { - public typealias RawValue = String - public var rawValue: String - public init(rawValue: String) { - self.rawValue = rawValue - } - - static let refreshRequestIdKey: NotificationUserInfoKey = .init(rawValue: "refreshRequestId") - static let refreshedRoutesResultKey: NotificationUserInfoKey = .init(rawValue: "refreshedRoutesResultKey") - static let legIndexKey: NotificationUserInfoKey = .init(rawValue: "legIndex") - static let refreshRequestErrorKey: NotificationUserInfoKey = .init(rawValue: "refreshRequestError") - - /// A key in the user info dictionary of a - /// ``Foundation/NSNotification/Name/navigationDidSwitchToFallbackVersion`` or - /// ``Foundation/NSNotification/Name/navigationDidSwitchToTargetVersion`` notification. The corresponding value - /// is a string representation of selected tiles version. - /// - /// For internal use only. - @_documentation(visibility: internal) - public static let tilesVersionKey: NotificationUserInfoKey = .init(rawValue: "tilesVersion") - - static let originKey: NotificationUserInfoKey = .init(rawValue: "origin") - - static let statusKey: NotificationUserInfoKey = .init(rawValue: "status") - - static let alternativesListKey: NotificationUserInfoKey = .init(rawValue: "alternativesList") - - static let removedAlternativesKey: NotificationUserInfoKey = .init(rawValue: "removedAlternatives") - - static let messageKey: NotificationUserInfoKey = .init(rawValue: "message") - - static let coincideOnlineRouteKey: NotificationUserInfoKey = .init(rawValue: "coincideOnlineRoute") - } -} - -extension RoadGraph { - /// Keys in the user info dictionaries of various notifications posted about ``RoadGraph``s. - public struct NotificationUserInfoKey: Hashable, Equatable, RawRepresentable, Sendable { - public typealias RawValue = String - public var rawValue: String - public init(rawValue: String) { - self.rawValue = rawValue - } - - /// A key in the user info dictionary of a ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition`` - /// notification. The corresponding value is a ``RoadGraph/Position`` indicating the current position in the - /// road graph. - public static let positionKey: NotificationUserInfoKey = .init(rawValue: "position") - - /// A key in the user info dictionary of a ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition`` - /// notification. The corresponding value is an ``RoadGraph/Edge`` at the root of a tree of edges in the routing - /// graph. This graph represents a probable path (or paths) of a vehicle within the routing graph for a certain - /// distance in front of the vehicle, thus extending the user’s perspective beyond the “visible” horizon as the - /// vehicle’s position and trajectory change. - public static let treeKey: NotificationUserInfoKey = .init(rawValue: "tree") - - /// A key in the user info dictionary of a ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition`` - /// notification. The corresponding value is a Boolean value of `true` if the position update indicates a new - /// most probable path (MPP) or `false` if it updates an existing MPP that the user has continued to follow. - /// - /// An electronic horizon can represent a new MPP in three scenarios: - /// - An electronic horizon is detected for the very first time. - /// - A user location tracking error leads to an MPP completely distinct from the previous MPP. - /// - The user has departed from the previous MPP, for example by driving to a side path of the previous MPP. - public static let updatesMostProbablePathKey: NotificationUserInfoKey = - .init(rawValue: "updatesMostProbablePath") - - /// A key in the user info dictionary of a ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition`` - /// notification. The corresponding value is an array of upcoming road object distances from the user’s current - /// location as ``DistancedRoadObject`` values. - public static let distancesByRoadObjectKey: NotificationUserInfoKey = .init(rawValue: "distancesByRoadObject") - - /// A key in the user info dictionary of a - /// ``Foundation/NSNotification/Name/electronicHorizonDidEnterRoadObject`` or - /// ``Foundation/NSNotification/Name/electronicHorizonDidExitRoadObject`` notification. The corresponding value - /// is a - /// ``RoadObject/Identifier`` identifying the road object that the user entered or exited. - public static let roadObjectIdentifierKey: NotificationUserInfoKey = .init(rawValue: "roadObjectIdentifier") - - /// A key in the user info dictionary of a - /// ``Foundation/NSNotification/Name/electronicHorizonDidEnterRoadObject`` or - /// ``Foundation/NSNotification/Name/electronicHorizonDidExitRoadObject`` notification. The corresponding value - /// is an `NSNumber` containing a Boolean value set to `true` if the user entered at the beginning or exited at - /// the end of the road object, or `false` if they entered or exited somewhere along the road object. - public static let didTransitionAtEndpointKey: NotificationUserInfoKey = - .init(rawValue: "didTransitionAtEndpoint") - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Environment.swift b/ios/Classes/Navigation/MapboxNavigationCore/Environment.swift deleted file mode 100644 index 334a2fd86..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Environment.swift +++ /dev/null @@ -1,13 +0,0 @@ -struct Environment: Sendable { - var audioPlayerClient: AudioPlayerClient -} - -extension Environment { - @AudioPlayerActor - static let live = Environment( - audioPlayerClient: .liveValue() - ) -} - -@AudioPlayerActor -var Current = Environment.live diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AVAudioSession.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AVAudioSession.swift deleted file mode 100644 index 0e6cd76af..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AVAudioSession.swift +++ /dev/null @@ -1,27 +0,0 @@ -import AVFoundation - -extension AVAudioSession { - // MARK: Adjusting the Volume - - public func tryDuckAudio() -> Error? { - do { - try setCategory(.playback, mode: .voicePrompt, options: [.duckOthers, .mixWithOthers]) - try setActive(true) - } catch { - return error - } - return nil - } - - public func tryUnduckAudio() -> Error? { - do { - try setActive( - false, - options: [.notifyOthersOnDeactivation] - ) - } catch { - return error - } - return nil - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AmenityType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AmenityType.swift deleted file mode 100644 index 411668eaa..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/AmenityType.swift +++ /dev/null @@ -1,51 +0,0 @@ -import MapboxDirections -import MapboxNavigationNative - -extension MapboxDirections.AmenityType { - init(_ native: MapboxNavigationNative.AmenityType) { - switch native { - case .undefined: - self = .undefined - case .gasStation: - self = .gasStation - case .electricChargingStation: - self = .electricChargingStation - case .toilet: - self = .toilet - case .coffee: - self = .coffee - case .restaurant: - self = .restaurant - case .snack: - self = .snack - case .ATM: - self = .ATM - case .info: - self = .info - case .babyCare: - self = .babyCare - case .facilitiesForDisabled: - self = .facilitiesForDisabled - case .shop: - self = .shop - case .telephone: - self = .telephone - case .hotel: - self = .hotel - case .hotspring: - self = .hotSpring - case .shower: - self = .shower - case .picnicShelter: - self = .picnicShelter - case .post: - self = .post - case .FAX: - self = .fax - @unknown default: - self = .undefined - Log.fault("Unexpected amenity type.", category: .navigation) - assertionFailure("Unexpected amenity type.") - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Array++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Array++.swift deleted file mode 100644 index 5c5eac758..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Array++.swift +++ /dev/null @@ -1,37 +0,0 @@ -import CoreLocation -import Turf - -@_spi(MapboxInternal) -extension Array where Iterator.Element == CLLocationCoordinate2D { - public func sliced( - from: CLLocationCoordinate2D? = nil, - to: CLLocationCoordinate2D? = nil - ) -> [CLLocationCoordinate2D] { - return LineString(self).sliced(from: from, to: to)?.coordinates ?? [] - } - - public func distance( - from: CLLocationCoordinate2D? = nil, - to: CLLocationCoordinate2D? = nil - ) -> CLLocationDistance? { - return LineString(self).distance(from: from, to: to) - } - - public func trimmed( - from: CLLocationCoordinate2D? = nil, - distance: CLLocationDistance - ) -> [CLLocationCoordinate2D] { - if let fromCoord = from ?? first { - return LineString(self).trimmed(from: fromCoord, distance: distance)?.coordinates ?? [] - } else { - return [] - } - } - - public var centerCoordinate: CLLocationCoordinate2D { - let avgLat = map(\.latitude).reduce(0.0, +) / Double(count) - let avgLng = map(\.longitude).reduce(0.0, +) / Double(count) - - return CLLocationCoordinate2D(latitude: avgLat, longitude: avgLng) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/BoundingBox++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/BoundingBox++.swift deleted file mode 100644 index 36e09cb17..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/BoundingBox++.swift +++ /dev/null @@ -1,15 +0,0 @@ -import CoreGraphics -import Turf - -extension BoundingBox { - /// Returns zoom level inside of specific `CGSize`, in which `BoundingBox` was fit to. - func zoomLevel(fitTo size: CGSize) -> Double { - let latitudeFraction = (northEast.latitude.toRadians() - southWest.latitude.toRadians()) / .pi - let longitudeDiff = northEast.longitude - southWest.longitude - let longitudeFraction = ((longitudeDiff < 0) ? (longitudeDiff + 360) : longitudeDiff) / 360 - let latitudeZoom = log(Double(size.height) / 512.0 / latitudeFraction) / M_LN2 - let longitudeZoom = log(Double(size.width) / 512.0 / longitudeFraction) / M_LN2 - - return min(latitudeZoom, longitudeZoom, 21.0) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Bundle.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Bundle.swift deleted file mode 100644 index 134956b2e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Bundle.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import UIKit - -private final class BundleToken {} - -extension Bundle { - // MARK: Accessing Mapbox-Specific Bundles - - /// Returns a set of strings containing supported background mode types. - public var backgroundModes: Set { - if let modes = object(forInfoDictionaryKey: "UIBackgroundModes") as? [String] { - return Set(modes) - } - return [] - } - - var locationAlwaysAndWhenInUseUsageDescription: String? { - return object(forInfoDictionaryKey: "NSLocationAlwaysAndWhenInUseUsageDescription") as? String - } - - var locationWhenInUseUsageDescription: String? { - return object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") as? String - } - -#if !SWIFT_PACKAGE - private static let module: Bundle = .init(for: BundleToken.self) -#endif - - /// The Mapbox Core Navigation framework bundle. - public static let mapboxNavigationUXCore: Bundle = .module - - /// Provides `Bundle` instance, based on provided bundle name and class inside of it. - /// - Parameters: - /// - bundleName: Name of the bundle. - /// - class: Class, which is located inside of the bundle. - /// - Returns: Instance of the bundle if it was found, otherwise `nil`. - static func bundle(for bundleName: String, class: AnyClass) -> Bundle? { - let candidates = [ - // Bundle should be present here when the package is linked into an App. - Bundle.main.resourceURL, - - // Bundle should be present here when the package is linked into a framework. - Bundle(for: `class`).resourceURL, - ] - - for candidate in candidates { - let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle") - if let bundle = bundlePath.flatMap(Bundle.init(url:)) { - return bundle - } - } - - return nil - } - - public func image(named: String) -> UIImage? { - guard let image = UIImage(named: named, in: self, compatibleWith: nil) else { - assertionFailure("Image \(named) wasn't found in Core Framework bundle") - return nil - } - return image - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CLLocationDirection++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CLLocationDirection++.swift deleted file mode 100644 index 40479f639..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CLLocationDirection++.swift +++ /dev/null @@ -1,9 +0,0 @@ -import CoreLocation - -extension CLLocationDirection { - /// Returns shortest rotation between two angles. - func shortestRotation(angle: CLLocationDirection) -> CLLocationDirection { - guard !isNaN, !angle.isNaN else { return 0.0 } - return (self - angle).wrap(min: -180.0, max: 180.0) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CongestionLevel.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CongestionLevel.swift deleted file mode 100644 index 10ecb2896..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/CongestionLevel.swift +++ /dev/null @@ -1,118 +0,0 @@ -import CarPlay -import Foundation -import MapboxDirections - -/// Range of numeric values determining congestion level. -/// -/// Congestion ranges work with `NumericCongestionLevel` values that can be requested by specifying -/// `AttributeOptions.numericCongestionLevel` in `DirectionOptions.attributes` when making Directions request. -public typealias CongestionRange = Range - -/// Configuration for connecting numeric congestion values to range categories. -public struct CongestionRangesConfiguration: Equatable, Sendable { - /// Numeric range for low congestion. - public var low: CongestionRange - /// Numeric range for moderate congestion. - public var moderate: CongestionRange - /// Numeric range for heavy congestion. - public var heavy: CongestionRange - /// Numeric range for severe congestion. - public var severe: CongestionRange - - /// Creates a new ``CongestionRangesConfiguration`` instance. - public init(low: CongestionRange, moderate: CongestionRange, heavy: CongestionRange, severe: CongestionRange) { - precondition(low.lowerBound >= 0, "Congestion level ranges can't include negative values.") - precondition( - low.upperBound <= moderate.lowerBound, - "Values from the moderate congestion level range can't intersect with or be lower than ones from the low congestion level range." - ) - precondition( - moderate.upperBound <= heavy.lowerBound, - "Values from the heavy congestion level range can't intersect with or be lower than ones from the moderate congestion level range." - ) - precondition( - heavy.upperBound <= severe.lowerBound, - "Values from the severe congestion level range can't intersect with or be lower than ones from the heavy congestion level range." - ) - precondition(severe.upperBound <= 101, "Congestion level ranges can't include values greater than 100.") - - self.low = low - self.moderate = moderate - self.heavy = heavy - self.severe = severe - } - - /// Default congestion ranges configuration. - public static var `default`: Self { - .init( - low: 0..<40, - moderate: 40..<60, - heavy: 60..<80, - severe: 80..<101 - ) - } -} - -extension CongestionLevel { - init(numericValue: NumericCongestionLevel?, configuration: CongestionRangesConfiguration) { - guard let numericValue else { - self = .unknown - return - } - - switch numericValue { - case configuration.low: - self = .low - case configuration.moderate: - self = .moderate - case configuration.heavy: - self = .heavy - case configuration.severe: - self = .severe - default: - self = .unknown - } - } - - /// Converts a CongestionLevel to a CPTimeRemainingColor. - public var asCPTimeRemainingColor: CPTimeRemainingColor { - switch self { - case .unknown: - return .default - case .low: - return .green - case .moderate: - return .orange - case .heavy: - return .red - case .severe: - return .red - } - } -} - -extension RouteLeg { - /// An array containing the traffic congestion level along each road segment in the route leg geometry. - /// - /// The array is formed either by converting values of `segmentNumericCongestionLevels` to ``CongestionLevel`` type - /// (see ``CongestionRange``) or by taking `segmentCongestionLevels`, depending whether - /// `AttributeOptions.numericCongestionLevel` or `AttributeOptions.congestionLevel` was specified in - /// `DirectionsOptions.attributes` during route request. - /// - /// If both are present, `segmentNumericCongestionLevels` is preferred. - /// - /// If none are present, returns `nil`. - public func resolveCongestionLevels(using configuration: CongestionRangesConfiguration) -> [CongestionLevel]? { - let congestionLevels: [CongestionLevel]? = if let numeric = segmentNumericCongestionLevels { - numeric.map { numericValue in - CongestionLevel(numericValue: numericValue, configuration: configuration) - } - } else if let levels = segmentCongestionLevels { - levels - } else { - nil - } - - return congestionLevels - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Coordinate2D.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Coordinate2D.swift deleted file mode 100644 index dd7ac12eb..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Coordinate2D.swift +++ /dev/null @@ -1,15 +0,0 @@ -import CoreLocation -import Foundation -import MapboxCommon - -extension Coordinate2D { - convenience init(_ coordinate: CLLocationCoordinate2D) { - self.init(value: .init(latitude: coordinate.latitude, longitude: coordinate.longitude)) - } -} - -extension CLLocation { - convenience init(_ coordinate: Coordinate2D) { - self.init(latitude: coordinate.value.latitude, longitude: coordinate.value.longitude) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Date.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Date.swift deleted file mode 100644 index 9bca2c195..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Date.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -extension Date { - var ISO8601: String { - return Date.ISO8601Formatter.string(from: self) - } - - static let ISO8601Formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - formatter.timeZone = TimeZone(secondsFromGMT: 0) - return formatter - }() - - var nanosecondsSince1970: Double { - // UnitDuration.nanoseconds requires iOS 13 - return timeIntervalSince1970 * 1e9 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Dictionary.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Dictionary.swift deleted file mode 100644 index b9b14ca6c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Dictionary.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -extension Dictionary where Value == Any { - mutating func deepMerge(with dictionary: Dictionary, uniquingKeysWith combine: @escaping (Value, Value) -> Value) { - merge(dictionary) { current, new in - guard var currentDict = current as? Dictionary, let newDict = new as? Dictionary else { - return combine(current, new) - } - currentDict.deepMerge(with: newDict, uniquingKeysWith: combine) - return currentDict - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/FixLocation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/FixLocation.swift deleted file mode 100644 index 99781d42d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/FixLocation.swift +++ /dev/null @@ -1,39 +0,0 @@ -import CoreLocation -import Foundation -import MapboxNavigationNative - -extension FixLocation { - convenience init(_ location: CLLocation, isMock: Bool = false) { - let bearingAccuracy = location.courseAccuracy >= 0 ? location.courseAccuracy as NSNumber : nil - - var provider: String? -#if compiler(>=5.5) - if #available(iOS 15.0, *) { - if let sourceInformation = location.sourceInformation { - // in some scenarios we store this information to history files, so to save space there, we use "short" - // names and 1/0 instead of true/false - let isSimulated = sourceInformation.isSimulatedBySoftware ? 1 : 0 - let isProducedByAccessory = sourceInformation.isProducedByAccessory ? 1 : 0 - - provider = "sim:\(isSimulated),acc:\(isProducedByAccessory)" - } - } -#endif - - self.init( - coordinate: location.coordinate, - monotonicTimestampNanoseconds: Int64(location.timestamp.nanosecondsSince1970), - time: location.timestamp, - speed: location.speed >= 0 ? location.speed as NSNumber : nil, - bearing: location.course >= 0 ? location.course as NSNumber : nil, - altitude: location.altitude as NSNumber, - accuracyHorizontal: location.horizontalAccuracy >= 0 ? location.horizontalAccuracy as NSNumber : nil, - provider: provider, - bearingAccuracy: bearingAccuracy, - speedAccuracy: location.speedAccuracy >= 0 ? location.speedAccuracy as NSNumber : nil, - verticalAccuracy: location.verticalAccuracy >= 0 ? location.verticalAccuracy as NSNumber : nil, - extras: [:], - isMock: isMock - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Geometry.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Geometry.swift deleted file mode 100644 index 7164722c9..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Geometry.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation -import MapboxCommon -import MapboxNavigationNative -import Turf - -extension Turf.Geometry { - init(_ native: MapboxCommon.Geometry) { - switch native.geometryType { - case GeometryType_Point: - if let point = native.extractLocations()?.locationValue { - self = .point(Point(point)) - } else { - preconditionFailure("Point can't be constructed. Geometry wasn't extracted.") - } - case GeometryType_Line: - if let coordinates = native.extractLocationsArray()?.map(\.locationValue) { - self = .lineString(LineString(coordinates)) - } else { - preconditionFailure("LineString can't be constructed. Geometry wasn't extracted.") - } - case GeometryType_Polygon: - if let coordinates = native.extractLocations2DArray()?.map({ $0.map(\.locationValue) }) { - self = .polygon(Polygon(coordinates)) - } else { - preconditionFailure("Polygon can't be constructed. Geometry wasn't extracted.") - } - case GeometryType_MultiPoint: - if let coordinates = native.extractLocationsArray()?.map(\.locationValue) { - self = .multiPoint(MultiPoint(coordinates)) - } else { - preconditionFailure("MultiPoint can't be constructed. Geometry wasn't extracted.") - } - case GeometryType_MultiLine: - if let coordinates = native.extractLocations2DArray()?.map({ $0.map(\.locationValue) }) { - self = .multiLineString(MultiLineString(coordinates)) - } else { - preconditionFailure("MultiLineString can't be constructed. Geometry wasn't extracted.") - } - case GeometryType_MultiPolygon: - if let coordinates = native.extractLocations3DArray()?.map({ $0.map { $0.map(\.locationValue) } }) { - self = .multiPolygon(MultiPolygon(coordinates)) - } else { - preconditionFailure("MultiPolygon can't be constructed. Geometry wasn't extracted.") - } - case GeometryType_GeometryCollection: - if let geometries = native.extractGeometriesArray()?.compactMap(Geometry.init) { - self = .geometryCollection(GeometryCollection(geometries: geometries)) - } else { - preconditionFailure("GeometryCollection can't be constructed. Geometry wasn't extracted.") - } - case GeometryType_Empty: - fallthrough - default: - preconditionFailure("Geometry can't be constructed. Unknown type.") - } - } -} - -extension NSValue { - var locationValue: CLLocationCoordinate2D { - let point = cgPointValue - return CLLocationCoordinate2DMake(Double(point.x), Double(point.y)) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Incident.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Incident.swift deleted file mode 100644 index da8380714..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Incident.swift +++ /dev/null @@ -1,80 +0,0 @@ -import Foundation -import MapboxDirections -import MapboxNavigationNative - -extension Incident { - init(_ incidentInfo: IncidentInfo) { - let incidentType: Incident.Kind - switch incidentInfo.type { - case .accident: - incidentType = .accident - case .congestion: - incidentType = .congestion - case .construction: - incidentType = .construction - case .disabledVehicle: - incidentType = .disabledVehicle - case .laneRestriction: - incidentType = .laneRestriction - case .massTransit: - incidentType = .massTransit - case .miscellaneous: - incidentType = .miscellaneous - case .otherNews: - incidentType = .otherNews - case .plannedEvent: - incidentType = .plannedEvent - case .roadClosure: - incidentType = .roadClosure - case .roadHazard: - incidentType = .roadHazard - case .weather: - incidentType = .weather - @unknown default: - assertionFailure("Unknown IncidentInfo type.") - incidentType = .undefined - } - - self.init( - identifier: incidentInfo.id, - type: incidentType, - description: incidentInfo.description ?? "", - creationDate: incidentInfo.creationTime ?? Date.distantPast, - startDate: incidentInfo.startTime ?? Date.distantPast, - endDate: incidentInfo.endTime ?? Date.distantPast, - impact: .init(incidentInfo.impact), - subtype: incidentInfo.subType, - subtypeDescription: incidentInfo.subTypeDescription, - alertCodes: Set(incidentInfo.alertcCodes.map(\.intValue)), - lanesBlocked: BlockedLanes(descriptions: incidentInfo.lanesBlocked), - shapeIndexRange: -1 ..< -1, - countryCodeAlpha3: incidentInfo.iso_3166_1_alpha3, - countryCode: incidentInfo.iso_3166_1_alpha2, - roadIsClosed: incidentInfo.roadClosed, - longDescription: incidentInfo.longDescription, - numberOfBlockedLanes: incidentInfo.numLanesBlocked?.intValue, - congestionLevel: incidentInfo.congestion?.value?.intValue, - affectedRoadNames: incidentInfo.affectedRoadNames - ) - } -} - -extension Incident.Impact { - init(_ incidentImpact: IncidentImpact) { - switch incidentImpact { - case .unknown: - self = .unknown - case .critical: - self = .critical - case .major: - self = .major - case .minor: - self = .minor - case .low: - self = .low - @unknown default: - assertionFailure("Unknown IncidentImpact value.") - self = .unknown - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Locale.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Locale.swift deleted file mode 100644 index 3eb52c7dd..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Locale.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -extension Locale { - /// Given the app's localized language setting, returns a string representing the user's localization. - public static var preferredLocalLanguageCountryCode: String { - let firstBundleLocale = Bundle.main.preferredLocalizations.first! - let bundleLocale = firstBundleLocale.components(separatedBy: "-") - - if bundleLocale.count > 1 { - return firstBundleLocale - } - - if let countryCode = (Locale.current as NSLocale).object(forKey: .countryCode) as? String { - return "\(bundleLocale.first!)-\(countryCode)" - } - - return firstBundleLocale - } - - /// Returns a `Locale` from ``Foundation/Locale/preferredLocalLanguageCountryCode``. - public static var nationalizedCurrent: Locale { - Locale(identifier: preferredLocalLanguageCountryCode) - } - - var BCP47Code: String { - if #available(iOS 16, *) { - language.maximalIdentifier - } else { - languageCode ?? identifier - } - } - - var preferredBCP47Codes: [String] { - let currentCode = BCP47Code - var codes = [currentCode] - for code in Self.preferredLanguages { - let newCode: String = if #available(iOS 16, *) { - Locale(languageCode: Locale.LanguageCode(stringLiteral: code)).BCP47Code - } else { - Locale(identifier: code).BCP47Code - } - guard newCode != currentCode else { continue } - codes.append(newCode) - } - return codes - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MapboxStreetsRoadClass.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MapboxStreetsRoadClass.swift deleted file mode 100644 index bf26c5454..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MapboxStreetsRoadClass.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import MapboxDirections -import MapboxNavigationNative - -extension MapboxStreetsRoadClass { - /// Returns a Boolean value indicating whether the road class is for a highway entrance or exit ramp (slip road). - public var isRamp: Bool { - return self == .motorwayLink || self == .trunkLink || self == .primaryLink || self == .secondaryLink - } - - init(_ native: FunctionalRoadClass, isRamp: Bool) { - switch native { - case .motorway: - self = isRamp ? .motorwayLink : .motorway - case .trunk: - self = isRamp ? .trunkLink : .trunk - case .primary: - self = isRamp ? .primaryLink : .primary - case .secondary: - self = isRamp ? .secondaryLink : .secondary - case .tertiary: - self = isRamp ? .tertiaryLink : .tertiary - case .unclassified, .residential: - // Mapbox Streets conflates unclassified and residential roads, because generally speaking they are - // distinguished only by their abutters; neither is “higher” than the other in priority. - self = .street - case .serviceOther: - self = .service - @unknown default: - assertionFailure("Unknown FunctionalRoadClass value.") - self = .undefined - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MeasurementSystem.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MeasurementSystem.swift deleted file mode 100644 index 86ef92a76..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/MeasurementSystem.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -import MapboxDirections - -extension MeasurementSystem { - /// Converts `LengthFormatter.Unit` into `MapboxDirections.MeasurementSystem`. - public init(_ lengthUnit: LengthFormatter.Unit) { - let metricUnits: [LengthFormatter.Unit] = [.kilometer, .centimeter, .meter, .millimeter] - self = metricUnits.contains(lengthUnit) ? .metric : .imperial - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/NavigationStatus.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/NavigationStatus.swift deleted file mode 100644 index 7b2fafafb..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/NavigationStatus.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation -import MapboxDirections -import MapboxNavigationNative - -extension NavigationStatus { - private static let nameSeparator = " / " - - func localizedRoadName(locale: Locale = .nationalizedCurrent) -> RoadName { - let roadNames = localizedRoadNames(locale: locale) - - let name = roadNames.first { $0.shield == nil } ?? nonLocalizedRoadName - let shield = localizedShield(locale: locale).map(RoadShield.init) - return .init(text: name.text, language: name.language, shield: shield) - } - - private var nonLocalizedRoadName: MapboxNavigationNative.RoadName { - let text = roads - .filter { $0.shield == nil } - .map(\.text) - .joined(separator: NavigationStatus.nameSeparator) - return .init(text: text, language: "", imageBaseUrl: nil, shield: nil) - } - - private func localizedShield(locale: Locale) -> Shield? { - let roadNames = localizedRoadNames(locale: locale) - return roadNames.compactMap(\.shield).first ?? shield - } - - private func localizedRoadNames(locale: Locale) -> [MapboxNavigationNative.RoadName] { - roads.filter { $0.language == locale.languageCode } - } - - private var shield: Shield? { - roads.compactMap(\.shield).first - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Preconcurrency+Sendable.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Preconcurrency+Sendable.swift deleted file mode 100644 index 9f493bf5c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Preconcurrency+Sendable.swift +++ /dev/null @@ -1,7 +0,0 @@ -import MapboxMaps -import Turf - -extension LineString: @unchecked Sendable {} - -extension Puck3DConfiguration: @unchecked Sendable {} -extension Puck2DConfiguration: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RestStop.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RestStop.swift deleted file mode 100644 index 49263a25d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RestStop.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import MapboxDirections -import MapboxNavigationNative - -extension RestStop { - init?(_ serviceArea: ServiceAreaInfo) { - let amenities: [MapboxDirections.Amenity] = serviceArea.amenities.map { amenity in - Amenity( - type: AmenityType(amenity.type), - name: amenity.name, - brand: amenity.brand - ) - } - - switch serviceArea.type { - case .restArea: - self.init(type: .restArea, name: serviceArea.name, amenities: amenities) - case .serviceArea: - self.init(type: .serviceArea, name: serviceArea.name, amenities: amenities) - @unknown default: - assertionFailure("Unknown ServiceAreaInfo type.") - return nil - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Result.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Result.swift deleted file mode 100644 index 753f38e60..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Result.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import MapboxCommon_Private - -extension Result { - init(expected: Expected) { - if expected.isValue(), let value = expected.value { - guard let typedValue = value as? Success else { - preconditionFailure("Result value can't be constructed. Unknown expected value type.") - } - self = .success(typedValue) - } else if expected.isError(), let error = expected.error { - guard let typedError = error as? Failure else { - preconditionFailure("Result error can't be constructed. Unknown expected error type.") - } - self = .failure(typedError) - } else { - preconditionFailure("Expected type is neither a value nor an error.") - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteLeg.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteLeg.swift deleted file mode 100644 index cc0af2297..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteLeg.swift +++ /dev/null @@ -1,46 +0,0 @@ -import MapboxDirections -import Turf - -extension RouteLeg { - public var shape: LineString { - return steps.dropFirst().reduce(into: steps.first?.shape ?? LineString([])) { result, step in - result.coordinates += (step.shape?.coordinates ?? []).dropFirst() - } - } - - func mapIntersectionsAttributes(_ attributeTransform: (Intersection) -> T) -> [T] { - // Pick only valid segment indices for specific `Intersection` in `RouteStep`. - // Array of segment indexes can look like this: [0, 3, 24, 28, 48, 50, 51, 53]. - let segmentIndices = steps.compactMap { $0.segmentIndicesByIntersection?.compactMap { $0 } }.reduce([], +) - - // Pick selected attribute by `attributeTransform` in specific `Intersection` of `RouteStep`. - // It is expected that number of `segmentIndices` will be equal to number of `attributesInLeg`. - // Array may include optionals and nil values. - let attributesInLeg = steps.compactMap { $0.intersections?.map(attributeTransform) }.reduce([], +) - - // Map each selected attribute to the amount of two adjacent `segmentIndices`. - // At the end amount of attributes should be equal to the last segment index. - let streetsRoadClasses = segmentIndices.enumerated().map { - segmentIndices.indices.contains($0.offset + 1) && attributesInLeg.indices.contains($0.offset) ? - Array( - repeating: attributesInLeg[$0.offset], - count: segmentIndices[$0.offset + 1] - segmentIndices[$0.offset] - ) : [] - - }.reduce([], +) - - return streetsRoadClasses - } - - /// Returns an array of `MapboxStreetsRoadClass` objects for specific leg. `MapboxStreetsRoadClass` will be set to - /// `nil` if it's not present in `Intersection`. - public var streetsRoadClasses: [MapboxStreetsRoadClass?] { - return mapIntersectionsAttributes { $0.outletMapboxStreetsRoadClass } - } - - /// Returns an array of `RoadClasses` objects for specific leg. `RoadClasses` will be set to `nil` if it's not - /// present in `Intersection`. - public var roadClasses: [RoadClasses?] { - return mapIntersectionsAttributes { $0.outletRoadClasses } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteOptions.swift deleted file mode 100644 index 854be7e29..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/RouteOptions.swift +++ /dev/null @@ -1,62 +0,0 @@ -import CoreLocation -import MapboxDirections - -extension RouteOptions { - var activityType: CLActivityType { - switch profileIdentifier { - case .cycling, .walking: - return .fitness - default: - return .otherNavigation - } - } - - /// Returns a tuple containing the waypoints along the leg at the given index and the waypoints that separate - /// subsequent legs. - /// - /// The first element of the tuple includes the leg’s source but not its destination. - func waypoints(fromLegAt legIndex: Int) -> ([Waypoint], [Waypoint]) { - // The first and last waypoints always separate legs. Make exceptions for these waypoints instead of modifying - // them by side effect. - let legSeparators = waypoints.filterKeepingFirstAndLast { $0.separatesLegs } - let viaPointsByLeg = waypoints.splitExceptAtStartAndEnd(omittingEmptySubsequences: false) { $0.separatesLegs } - .dropFirst() // No leg precedes first separator. - - let reconstitutedWaypoints = zip(legSeparators, viaPointsByLeg).dropFirst(legIndex).map { [$0.0] + $0.1 } - let legWaypoints = reconstitutedWaypoints.first ?? [] - let subsequentWaypoints = reconstitutedWaypoints.dropFirst() - return (legWaypoints, subsequentWaypoints.flatMap { $0 }) - } -} - -extension RouteOptions { - /// Returns a copy of the route options by roundtripping through JSON. - /// - /// - Throws: An `EncodingError` or `DecodingError` if the route options could not be roundtripped through JSON. - func copy() throws -> Self { - // TODO: remove this method when changed to value type. - // Work around . - let encodedOptions = try JSONEncoder().encode(self) - return try JSONDecoder().decode(type(of: self), from: encodedOptions) - } -} - -extension Array { - /// - seealso: `Array.filter(_:)` - public func filterKeepingFirstAndLast(_ isIncluded: (Element) throws -> Bool) rethrows -> [Element] { - return try enumerated().filter { - try isIncluded($0.element) || $0.offset == 0 || $0.offset == indices.last - }.map(\.element) - } - - /// - seealso: `Array.split(maxSplits:omittingEmptySubsequences:whereSeparator:)` - public func splitExceptAtStartAndEnd( - maxSplits: Int = .max, - omittingEmptySubsequences: Bool = true, - whereSeparator isSeparator: (Element) throws -> Bool - ) rethrows -> [ArraySlice] { - return try enumerated().split(maxSplits: maxSplits, omittingEmptySubsequences: omittingEmptySubsequences) { - try isSeparator($0.element) || $0.offset == 0 || $0.offset == indices.last - }.map { $0.map(\.element).suffix(from: 0) } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/SpokenInstruction.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/SpokenInstruction.swift deleted file mode 100644 index 022c17ff7..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/SpokenInstruction.swift +++ /dev/null @@ -1,52 +0,0 @@ -import AVFoundation -import Foundation -import MapboxDirections - -extension SpokenInstruction { - func attributedText(for legProgress: RouteLegProgress) -> NSAttributedString { - let attributedText = NSMutableAttributedString(string: text) - if let step = legProgress.upcomingStep, - let name = step.names?.first, - let phoneticName = step.phoneticNames?.first - { - let nameRange = attributedText.mutableString.range(of: name) - if nameRange.location != NSNotFound { - attributedText.replaceCharacters( - in: nameRange, - with: NSAttributedString(string: name).pronounced(phoneticName) - ) - } - } - if let step = legProgress.followOnStep, - let name = step.names?.first, - let phoneticName = step.phoneticNames?.first - { - let nameRange = attributedText.mutableString.range(of: name) - if nameRange.location != NSNotFound { - attributedText.replaceCharacters( - in: nameRange, - with: NSAttributedString(string: name).pronounced(phoneticName) - ) - } - } - return attributedText - } -} - -extension NSAttributedString { - public func pronounced(_ pronunciation: String) -> NSAttributedString { - let phoneticWords = pronunciation.components(separatedBy: " ") - let phoneticString = NSMutableAttributedString() - for (word, phoneticWord) in zip(string.components(separatedBy: " "), phoneticWords) { - // AVSpeechSynthesizer doesn’t recognize some common IPA symbols. - let phoneticWord = phoneticWord.byReplacing([("ɡ", "g"), ("ɹ", "r")]) - if phoneticString.length > 0 { - phoneticString.append(NSAttributedString(string: " ")) - } - phoneticString.append(NSAttributedString(string: word, attributes: [ - NSAttributedString.Key(rawValue: AVSpeechSynthesisIPANotationAttribute): phoneticWord, - ])) - } - return phoneticString - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/String.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/String.swift deleted file mode 100644 index 3e293ca58..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/String.swift +++ /dev/null @@ -1,58 +0,0 @@ -import CommonCrypto -import Foundation - -extension String { - typealias Replacement = (of: String, with: String) - - func byReplacing(_ replacements: [Replacement]) -> String { - return replacements.reduce(self) { $0.replacingOccurrences(of: $1.of, with: $1.with) } - } - - /// Returns the SHA256 hash of the string. - var sha256: String { - let length = Int(CC_SHA256_DIGEST_LENGTH) - let digest = utf8CString.withUnsafeBufferPointer { body -> [UInt8] in - var digest = [UInt8](repeating: 0, count: length) - CC_SHA256(body.baseAddress, CC_LONG(lengthOfBytes(using: .utf8)), &digest) - return digest - } - return digest.lazy.map { String(format: "%02x", $0) }.joined() - } - - // Adapted from https://github.com/raywenderlich/swift-algorithm-club/blob/master/Minimum%20Edit%20Distance/MinimumEditDistance.playground/Contents.swift - public func minimumEditDistance(to word: String) -> Int { - let fromWordCount = count - let toWordCount = word.count - - guard !isEmpty else { return toWordCount } - guard !word.isEmpty else { return fromWordCount } - - var matrix = [[Int]](repeating: [Int](repeating: 0, count: toWordCount + 1), count: fromWordCount + 1) - - // initialize matrix - for index in 1...fromWordCount { - // the distance of any first string to an empty second string - matrix[index][0] = index - } - - for index in 1...toWordCount { - // the distance of any second string to an empty first string - matrix[0][index] = index - } - - // compute Levenshtein distance - for (i, selfChar) in enumerated() { - for (j, otherChar) in word.enumerated() { - if otherChar == selfChar { - // substitution of equal symbols with cost 0 - matrix[i + 1][j + 1] = matrix[i][j] - } else { - // minimum of the cost of insertion, deletion, or substitution - // added to the already computed costs in the corresponding cells - matrix[i + 1][j + 1] = Swift.min(matrix[i][j] + 1, matrix[i + 1][j] + 1, matrix[i][j + 1] + 1) - } - } - } - return matrix[fromWordCount][toWordCount] - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/TollCollection.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/TollCollection.swift deleted file mode 100644 index 7aad46a8e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/TollCollection.swift +++ /dev/null @@ -1,18 +0,0 @@ - -import Foundation -import MapboxDirections -import MapboxNavigationNative - -extension TollCollection { - init?(_ tollInfo: TollCollectionInfo) { - switch tollInfo.type { - case .tollBooth: - self.init(type: .booth, name: tollInfo.name) - case .tollGantry: - self.init(type: .gantry, name: tollInfo.name) - @unknown default: - assertionFailure("Unknown TollCollectionInfo type.") - return nil - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIDevice.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIDevice.swift deleted file mode 100644 index 625f2d6e1..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIDevice.swift +++ /dev/null @@ -1,22 +0,0 @@ -import UIKit - -extension UIDevice { - static var isSimulator: Bool { -#if targetEnvironment(simulator) - return true -#else - return false -#endif - } - - var screenOrientation: UIDeviceOrientation { - let screenOrientation: UIDeviceOrientation = if orientation.isValidInterfaceOrientation { - orientation - } else if UIScreen.main.bounds.height > UIScreen.main.bounds.width { - .portrait - } else { - .landscapeLeft - } - return screenOrientation - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIEdgeInsets.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIEdgeInsets.swift deleted file mode 100644 index 8ae0b7e2c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/UIEdgeInsets.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import UIKit - -extension UIEdgeInsets { - static func + (left: UIEdgeInsets, right: UIEdgeInsets) -> UIEdgeInsets { - return UIEdgeInsets( - top: left.top + right.top, - left: left.left + right.left, - bottom: left.bottom + right.bottom, - right: left.right + right.right - ) - } - - static func += (lhs: inout UIEdgeInsets, rhs: UIEdgeInsets) { - lhs.top += rhs.top - lhs.left += rhs.left - lhs.bottom += rhs.bottom - lhs.right += rhs.right - } - - func rectValue(_ rect: CGRect) -> CGRect { - return CGRect( - x: rect.origin.x + left, - y: rect.origin.y + top, - width: rect.size.width - left - right, - height: rect.size.height - top - bottom - ) - } - - static var centerEdgeInsets: UIEdgeInsets { - return UIEdgeInsets(top: 10.0, left: 20.0, bottom: 10.0, right: 20.0) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Utils.swift b/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Utils.swift deleted file mode 100644 index 5b21ab506..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Extensions/Utils.swift +++ /dev/null @@ -1,57 +0,0 @@ -import CoreGraphics -import CoreLocation -import Foundation - -private let tileSize: Double = 512.0 -private let M2PI = Double.pi * 2 -private let MPI2 = Double.pi / 2 -private let DEG2RAD = Double.pi / 180.0 -private let EARTH_RADIUS_M = 6378137.0 -private let LATITUDE_MAX: Double = 85.051128779806604 -private let AngularFieldOfView: CLLocationDegrees = 30 -private let MIN_ZOOM = 0.0 -private let MAX_ZOOM = 25.5 - -func AltitudeForZoomLevel( - _ zoomLevel: Double, - _ pitch: CGFloat, - _ latitude: CLLocationDegrees, - _ size: CGSize -) -> CLLocationDistance { - let metersPerPixel = getMetersPerPixelAtLatitude(latitude, zoomLevel) - let metersTall = metersPerPixel * Double(size.height) - let altitude = metersTall / 2 / tan(RadiansFromDegrees(AngularFieldOfView) / 2) - return altitude * sin(MPI2 - RadiansFromDegrees(CLLocationDegrees(pitch))) / sin(MPI2) -} - -func ZoomLevelForAltitude( - _ altitude: CLLocationDistance, - _ pitch: CGFloat, - _ latitude: CLLocationDegrees, - _ size: CGSize -) -> Double { - let eyeAltitude = altitude / sin(MPI2 - RadiansFromDegrees(CLLocationDegrees(pitch))) * sin(MPI2) - let metersTall = eyeAltitude * 2 * tan(RadiansFromDegrees(AngularFieldOfView) / 2) - let metersPerPixel = metersTall / Double(size.height) - let mapPixelWidthAtZoom = cos(RadiansFromDegrees(latitude)) * M2PI * EARTH_RADIUS_M / metersPerPixel - return log2(mapPixelWidthAtZoom / tileSize) -} - -private func clamp(_ value: Double, _ min: Double, _ max: Double) -> Double { - return fmax(min, fmin(max, value)) -} - -private func worldSize(_ scale: Double) -> Double { - return scale * tileSize -} - -private func RadiansFromDegrees(_ degrees: CLLocationDegrees) -> Double { - return degrees * Double.pi / 180 -} - -func getMetersPerPixelAtLatitude(_ lat: Double, _ zoom: Double) -> Double { - let constrainedZoom = clamp(zoom, MIN_ZOOM, MAX_ZOOM) - let constrainedScale = pow(2.0, constrainedZoom) - let constrainedLatitude = clamp(lat, -LATITUDE_MAX, LATITUDE_MAX) - return cos(constrainedLatitude * DEG2RAD) * M2PI * EARTH_RADIUS_M / worldSize(constrainedScale) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/ActiveNavigationFeedbackType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/ActiveNavigationFeedbackType.swift deleted file mode 100644 index e9c9708f8..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/ActiveNavigationFeedbackType.swift +++ /dev/null @@ -1,79 +0,0 @@ -import Foundation - -/// Feedback type is used to specify the type of feedback being recorded with -/// ``NavigationEventsManager/sendActiveNavigationFeedback(_:type:description:)``. -public enum ActiveNavigationFeedbackType: FeedbackType { - case closure - case poorRoute - case wrongSpeedLimit - - case badRoute - case illegalTurn - case roadClosed - case incorrectLaneGuidance - case other - case arrival(rating: Int) - - case falsePositiveTraffic - case falseNegativeTraffic - case missingConstruction - case missingSpeedLimit - - /// Description of the category for this type of feedback - public var typeKey: String { - switch self { - case .closure: - return "ag_missing_closure" - case .poorRoute: - return "ag_poor_route" - case .wrongSpeedLimit: - return "ag_wrong_speed_limit" - case .badRoute: - return "routing_error" - case .illegalTurn: - return "turn_was_not_allowed" - case .roadClosed: - return "road_closed" - case .incorrectLaneGuidance: - return "lane_guidance_incorrect" - case .other: - return "other_navigation" - case .arrival: - return "arrival" - case .falsePositiveTraffic: - return "ag_fp_traffic" - case .falseNegativeTraffic: - return "ag_fn_traffic" - case .missingConstruction: - return "ag_missing_construction" - case .missingSpeedLimit: - return "ag_missing_speed_limit" - } - } - - /// Optional detailed description of the subtype of this feedback - public var subtypeKey: String? { - if case .arrival(let rating) = self { - return String(rating) - } - return nil - } -} - -/// Enum denoting the origin source of the corresponding feedback item -public enum FeedbackSource: Int, CustomStringConvertible, Sendable { - case user - case reroute - case unknown - - public var description: String { - switch self { - case .user: - return "user" - case .reroute: - return "reroute" - case .unknown: - return "unknown" - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventFixLocation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventFixLocation.swift deleted file mode 100644 index 9221e7a6e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventFixLocation.swift +++ /dev/null @@ -1,119 +0,0 @@ -import CoreLocation -import Foundation -import MapboxNavigationNative - -struct EventFixLocation { - let coordinate: CLLocationCoordinate2D - let altitude: CLLocationDistance? - let time: Date - let monotonicTimestampNanoseconds: Int64 - let horizontalAccuracy: CLLocationAccuracy? - let verticalAccuracy: CLLocationAccuracy? - let bearingAccuracy: CLLocationDirectionAccuracy? - let speedAccuracy: CLLocationSpeedAccuracy? - let bearing: CLLocationDirection? - let speed: CLLocationSpeed? - let provider: String? - let extras: [String: Any] - let isMock: Bool - - /// Initializes an event location consistent with the given location object. - init(_ location: FixLocation) { - self.coordinate = location.coordinate - self.altitude = location.altitude?.doubleValue - self.time = location.time - self.monotonicTimestampNanoseconds = location.monotonicTimestampNanoseconds - self.speed = location.speed?.doubleValue - self.bearing = location.bearing?.doubleValue - self.bearingAccuracy = location.bearingAccuracy?.doubleValue - self.horizontalAccuracy = location.accuracyHorizontal?.doubleValue - self.verticalAccuracy = location.verticalAccuracy?.doubleValue - self.speedAccuracy = location.speedAccuracy?.doubleValue - self.provider = location.provider - self.extras = location.extras - self.isMock = location.isMock - } -} - -extension EventFixLocation: Codable { - private enum CodingKeys: String, CodingKey { - case latitude = "lat" - case longitude = "lon" - case monotonicTimestampNanoseconds - case time - case speed - case bearing - case altitude - case accuracyHorizontal - case provider - case bearingAccuracy - case speedAccuracy - case verticalAccuracy - case extras - case isMock - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let latitude = try container.decode(CLLocationDegrees.self, forKey: .latitude) - let longitude = try container.decode(CLLocationDegrees.self, forKey: .longitude) - let extrasData = try container.decode(Data.self, forKey: .extras) - - let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - let fixLocation = try FixLocation( - coordinate: coordinate, - monotonicTimestampNanoseconds: container.decode(Int64.self, forKey: .monotonicTimestampNanoseconds), - time: container.decode(Date.self, forKey: .time), - speed: container.decodeIfPresent(Double.self, forKey: .speed) as NSNumber?, - bearing: container.decodeIfPresent(Double.self, forKey: .bearing) as NSNumber?, - altitude: container.decodeIfPresent(Double.self, forKey: .altitude) as NSNumber?, - accuracyHorizontal: container.decodeIfPresent(Double.self, forKey: .accuracyHorizontal) as NSNumber?, - provider: container.decodeIfPresent(String.self, forKey: .provider), - bearingAccuracy: container.decodeIfPresent(Double.self, forKey: .bearingAccuracy) as NSNumber?, - speedAccuracy: container.decodeIfPresent(Double.self, forKey: .speedAccuracy) as NSNumber?, - verticalAccuracy: container.decodeIfPresent(Double.self, forKey: .verticalAccuracy) as NSNumber?, - extras: JSONSerialization.jsonObject(with: extrasData) as? [String: Any] ?? [:], - isMock: container.decode(Bool.self, forKey: .isMock) - ) - self.init(fixLocation) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(coordinate.latitude, forKey: .latitude) - try container.encode(coordinate.longitude, forKey: .longitude) - try container.encode(monotonicTimestampNanoseconds, forKey: .monotonicTimestampNanoseconds) - try container.encode(time, forKey: .time) - try container.encode(isMock, forKey: .isMock) - try container.encodeIfPresent(speed, forKey: .speed) - try container.encodeIfPresent(bearing, forKey: .bearing) - try container.encodeIfPresent(altitude, forKey: .altitude) - try container.encodeIfPresent(provider, forKey: .provider) - try container.encodeIfPresent(bearingAccuracy, forKey: .bearingAccuracy) - try container.encodeIfPresent(speedAccuracy, forKey: .speedAccuracy) - try container.encodeIfPresent(verticalAccuracy, forKey: .verticalAccuracy) - let extrasData = try JSONSerialization.data(withJSONObject: extras) - try container.encode(extrasData, forKey: .extras) - } -} - -extension FixLocation { - convenience init(_ location: EventFixLocation) { - self.init( - coordinate: location.coordinate, - monotonicTimestampNanoseconds: location.monotonicTimestampNanoseconds, - time: location.time, - speed: location.speed as NSNumber?, - bearing: location.bearing as NSNumber?, - altitude: location.altitude as NSNumber?, - accuracyHorizontal: location.horizontalAccuracy as NSNumber?, - provider: location.provider, - bearingAccuracy: location.bearingAccuracy as NSNumber?, - speedAccuracy: location.speedAccuracy as NSNumber?, - verticalAccuracy: location.verticalAccuracy as NSNumber?, - extras: location.extras, - isMock: location.isMock - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventStep.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventStep.swift deleted file mode 100644 index d5b1f3454..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventStep.swift +++ /dev/null @@ -1,55 +0,0 @@ -import CoreLocation -import Foundation -import MapboxNavigationNative - -struct EventStep: Equatable, Codable { - let distance: Double - let distanceRemaining: Double - let duration: Double - let durationRemaining: Double - let upcomingName: String - let upcomingType: String - let upcomingModifier: String - let upcomingInstruction: String - let previousName: String - let previousType: String - let previousModifier: String - let previousInstruction: String - - /// Initializes an event location consistent with the given location object. - init(_ step: Step) { - self.distance = step.distance - self.distanceRemaining = step.distanceRemaining - self.duration = step.duration - self.durationRemaining = step.durationRemaining - self.upcomingName = step.upcomingName - self.upcomingType = step.upcomingType - self.upcomingModifier = step.upcomingModifier - self.upcomingInstruction = step.upcomingInstruction - self.previousName = step.previousName - self.previousType = step.previousType - self.previousModifier = step.previousModifier - self.previousInstruction = step.previousInstruction - } -} - -extension Step { - convenience init?(_ step: EventStep?) { - guard let step else { return nil } - - self.init( - distance: step.distance, - distanceRemaining: step.distanceRemaining, - duration: step.duration, - durationRemaining: step.durationRemaining, - upcomingName: step.upcomingName, - upcomingType: step.upcomingType, - upcomingModifier: step.upcomingModifier, - upcomingInstruction: step.upcomingInstruction, - previousName: step.previousName, - previousType: step.previousType, - previousModifier: step.previousModifier, - previousInstruction: step.previousInstruction - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventsManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventsManager.swift deleted file mode 100644 index a6d926571..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/EventsManager.swift +++ /dev/null @@ -1,177 +0,0 @@ -import CoreLocation -import Foundation -import MapboxCommon -import MapboxNavigationNative_Private -import UIKit - -/// The ``NavigationEventsManager`` is responsible for being the liaison between MapboxCoreNavigation and the Mapbox -/// telemetry. -public final class NavigationEventsManager: Sendable { - let navNativeEventsManager: NavigationTelemetryManager? - - // MARK: Configuring Events - - /// Optional application metadata that that can help Mapbox more reliably diagnose problems that occur in the SDK. - /// For example, you can provide your application’s name and version, a unique identifier for the end user, and a - /// session identifier. - /// To include this information, use the following keys: "name", "version", "userId", and "sessionId". - public var userInfo: [String: String?]? { - get { navNativeEventsManager?.userInfo } - set { navNativeEventsManager?.userInfo = newValue } - } - - required init( - eventsMetadataProvider: EventsMetadataProvider, - telemetry: Telemetry - ) { - self.navNativeEventsManager = NavigationNativeEventsManager( - eventsMetadataProvider: eventsMetadataProvider, - telemetry: telemetry - ) - } - - init(navNativeEventsManager: NavigationTelemetryManager?) { - self.navNativeEventsManager = navNativeEventsManager - } - - // MARK: Sending Feedback Events - - /// Create feedback about the current road segment/maneuver to be sent to the Mapbox data team. - /// - /// You can pair this with a custom feedback UI in your app to flag problems during navigation such as road - /// closures, incorrect instructions, etc. - /// If you provide a custom feedback UI that lets users elaborate on an issue, you should call this before you show - /// the custom UI so the location and timestamp are more accurate. Alternatively, you can use - /// `FeedbackViewContoller` which handles feedback lifecycle internally. - /// - Parameter screenshotOption: The options to configure how the screenshot for the vent is provided. - /// - Returns: A ``FeedbackEvent``. - /// - Postcondition: Call ``sendActiveNavigationFeedback(_:type:description:)`` and - /// ``sendPassiveNavigationFeedback(_:type:description:)`` with the returned feedback to attach additional metadata - /// to the feedback and send it. - public func createFeedback(screenshotOption: FeedbackScreenshotOption = .automatic) async -> FeedbackEvent? { - await navNativeEventsManager?.createFeedback(screenshotOption: screenshotOption) - } - - /// You can pair this with a custom feedback UI in your app to flag problems during navigation such as road - /// closures, incorrect instructions, etc. - /// - Parameters: - /// - feedback: A ``FeedbackEvent`` created with ``createFeedback(screenshotOption:)`` method. - /// - type: An ``ActiveNavigationFeedbackType`` used to specify the type of feedback. - /// - description: A custom string used to describe the problem in detail. - public func sendActiveNavigationFeedback( - _ feedback: FeedbackEvent, - type: ActiveNavigationFeedbackType, - description: String? = nil - ) { - Task { - await sendActiveNavigationFeedback( - feedback, - type: type, - description: description, - source: .user - ) - } - } - - /// Send passive navigation feedback to the Mapbox data team. - /// - /// You can pair this with a custom feedback UI in your app to flag problems during navigation such as road - /// closures, incorrect instructions, etc. - /// - Parameters: - /// - feedback: A ``FeedbackEvent`` created with ``createFeedback(screenshotOption:)`` method. - /// - type: A ``PassiveNavigationFeedbackType`` used to specify the type of feedback. - /// - description: A custom string used to describe the problem in detail. - public func sendPassiveNavigationFeedback( - _ feedback: FeedbackEvent, - type: PassiveNavigationFeedbackType, - description: String? = nil - ) { - Task { - await sendPassiveNavigationFeedback( - feedback, - type: type, - description: description, - source: .user - ) - } - } - - /// Send active navigation feedback to the Mapbox data team. - /// - /// You can pair this with a custom feedback UI in your app to flag problems during navigation such as road - /// closures, incorrect instructions, etc. - /// - Parameters: - /// - feedback: A ``FeedbackEvent`` created with ``createFeedback(screenshotOption:)`` method. - /// - type: An ``ActiveNavigationFeedbackType`` used to specify the type of feedback. - /// - description: A custom string used to describe the problem in detail. - /// - source: A ``FeedbackSource`` used to specify feedback source. - /// - Returns: The sent ``UserFeedback``. - public func sendActiveNavigationFeedback( - _ feedback: FeedbackEvent, - type: ActiveNavigationFeedbackType, - description: String?, - source: FeedbackSource - ) async -> UserFeedback? { - return try? await navNativeEventsManager?.sendActiveNavigationFeedback( - feedback, - type: type, - description: description, - source: source - ) - } - - /// Send navigation feedback to the Mapbox data team. - /// - Parameters: - /// - feedback: A ``FeedbackEvent`` created with ``createFeedback(screenshotOption:)`` method. - /// - type: An ``FeedbackType`` used to specify the type of feedback. - /// - description: A custom string used to describe the problem in detail. - /// - source: A ``FeedbackSource`` used to specify feedback source. - /// - Returns: The sent ``UserFeedback``. - public func sendNavigationFeedback( - _ feedback: FeedbackEvent, - type: FeedbackType, - description: String?, - source: FeedbackSource - ) async throws -> UserFeedback? { - return try? await navNativeEventsManager?.sendNavigationFeedback( - feedback, - type: type, - description: description, - source: source - ) - } - - /// Send passive navigation feedback to the Mapbox data team. - /// - /// You can pair this with a custom feedback UI in your app to flag problems during navigation such as road - /// closures, incorrect instructions, etc. - /// - Parameters: - /// - feedback: A ``FeedbackEvent`` created with ``createFeedback(screenshotOption:)`` method. - /// - type: A ``PassiveNavigationFeedbackType`` used to specify the type of feedback. - /// - description: A custom string used to describe the problem in detail. - /// - source: A `FeedbackSource` used to specify feedback source. - /// - Returns: The sent ``UserFeedback``. - public func sendPassiveNavigationFeedback( - _ feedback: FeedbackEvent, - type: PassiveNavigationFeedbackType, - description: String?, - source: FeedbackSource - ) async -> UserFeedback? { - return try? await navNativeEventsManager?.sendPassiveNavigationFeedback( - feedback, - type: type, - description: description, - source: source - ) - } - - /// Send event that Car Play was connected. - public func sendCarPlayConnectEvent() { - navNativeEventsManager?.sendCarPlayConnectEvent() - } - - /// Send event that Car Play was disconnected. - public func sendCarPlayDisconnectEvent() { - navNativeEventsManager?.sendCarPlayDisconnectEvent() - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackEvent.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackEvent.swift deleted file mode 100644 index 312371e51..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackEvent.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -/// Feedback event that can be created using ``NavigationEventsManager/createFeedback(screenshotOption:)``. -/// Use ``NavigationEventsManager/sendActiveNavigationFeedback(_:type:description:)`` to send it to the server. -/// Conforms to the `Codable` protocol, so the application can store the event persistently. -public struct FeedbackEvent: Codable, Equatable, Sendable { - public let metadata: FeedbackMetadata - - init(metadata: FeedbackMetadata) { - self.metadata = metadata - } - - public var contents: [String: Any] { - return metadata.contents - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackMetadata.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackMetadata.swift deleted file mode 100644 index dc03696e4..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackMetadata.swift +++ /dev/null @@ -1,103 +0,0 @@ -import CoreLocation -import Foundation -import MapboxNavigationNative -import MapboxNavigationNative_Private - -public struct FeedbackMetadata: Sendable, Equatable { - public static func == (lhs: FeedbackMetadata, rhs: FeedbackMetadata) -> Bool { - let handlesAreEqual: Bool = switch (lhs.userFeedbackHandle, rhs.userFeedbackHandle) { - case (let lhsHandle as UserFeedbackHandle, let rhsHandle as UserFeedbackHandle): - lhsHandle == rhsHandle - default: - true - } - return handlesAreEqual && - lhs.calculatedUserFeedbackMetadata == rhs.calculatedUserFeedbackMetadata && - lhs.screenshot == rhs.screenshot - } - - private let userFeedbackHandle: (any NativeUserFeedbackHandle)? - private let calculatedUserFeedbackMetadata: UserFeedbackMetadata? - - var userFeedbackMetadata: UserFeedbackMetadata? { - calculatedUserFeedbackMetadata ?? userFeedbackHandle?.getMetadata() - } - - public let screenshot: String? - public var contents: [String: Any] { - guard let data = try? JSONEncoder().encode(self), - let dictionary = try? JSONSerialization.jsonObject( - with: data, options: .allowFragments - ) as? [String: Any] - else { - Log.warning("Unable to encode feedback event details", category: .navigation) - return [:] - } - return dictionary - } - - init( - userFeedbackHandle: (any NativeUserFeedbackHandle)?, - screenshot: String?, - userFeedbackMetadata: UserFeedbackMetadata? = nil - ) { - self.userFeedbackHandle = userFeedbackHandle - self.screenshot = screenshot - self.calculatedUserFeedbackMetadata = userFeedbackMetadata - } -} - -extension FeedbackMetadata: Codable { - fileprivate enum CodingKeys: String, CodingKey { - case screenshot - case locationsBefore - case locationsAfter - case step - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.screenshot = try container.decodeIfPresent(String.self, forKey: .screenshot) - self.calculatedUserFeedbackMetadata = try? UserFeedbackMetadata(from: decoder) - self.userFeedbackHandle = nil - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(screenshot, forKey: .screenshot) - try userFeedbackMetadata?.encode(to: encoder) - } -} - -protocol NativeUserFeedbackHandle: Sendable { - func getMetadata() -> UserFeedbackMetadata -} - -extension UserFeedbackHandle: NativeUserFeedbackHandle, @unchecked Sendable {} - -extension UserFeedbackMetadata: @unchecked Sendable {} - -extension UserFeedbackMetadata: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: FeedbackMetadata.CodingKeys.self) - let eventLocationsAfter: [EventFixLocation] = locationsAfter.map { .init($0) } - let eventLocationsBefore: [EventFixLocation] = locationsBefore.map { .init($0) } - let eventStep = step.map { EventStep($0) } - try container.encode(eventLocationsAfter, forKey: .locationsAfter) - try container.encode(eventLocationsBefore, forKey: .locationsBefore) - try container.encodeIfPresent(eventStep, forKey: .step) - } - - convenience init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: FeedbackMetadata.CodingKeys.self) - let locationsBefore = try container.decode([EventFixLocation].self, forKey: .locationsBefore) - let locationsAfter = try container.decode([EventFixLocation].self, forKey: .locationsAfter) - let eventStep = try container.decodeIfPresent(EventStep.self, forKey: .step) - - self.init( - locationsBefore: locationsBefore.map { FixLocation($0) }, - locationsAfter: locationsAfter.map { FixLocation($0) }, - step: Step(eventStep) - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackScreenshotOption.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackScreenshotOption.swift deleted file mode 100644 index 203022b95..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackScreenshotOption.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIKit - -/// Indicates screenshotting behavior of ``NavigationEventsManager``. -public enum FeedbackScreenshotOption: Sendable { - case automatic - case custom(UIImage) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackType.swift deleted file mode 100644 index fa5518d3a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/FeedbackType.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -/// Common protocol for ``ActiveNavigationFeedbackType`` and ``PassiveNavigationFeedbackType``. -public protocol FeedbackType: Sendable { - var typeKey: String { get } - var subtypeKey: String? { get } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/NavigationEventsManagerError.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/NavigationEventsManagerError.swift deleted file mode 100644 index 927a070c5..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/NavigationEventsManagerError.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -/// An error that occures during event sending. -@_spi(MapboxInternal) -public enum NavigationEventsManagerError: LocalizedError { - case failedToSend(reason: String) - case invalidData -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/PassiveNavigationFeedbackType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/PassiveNavigationFeedbackType.swift deleted file mode 100644 index 80bf038fd..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/PassiveNavigationFeedbackType.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -/// Feedback type is used to specify the type of feedback being recorded with -/// ``NavigationEventsManager/sendPassiveNavigationFeedback(_:type:description:)``. -public enum PassiveNavigationFeedbackType: FeedbackType { - case poorGPS - case incorrectMapData - case accident - case camera - case traffic - case wrongSpeedLimit - case other - - public var typeKey: String { - switch self { - case .other: - return "other_issue" - case .poorGPS: - return "fd_poor_gps" - case .incorrectMapData: - return "fd_incorrect_map_data" - case .accident: - return "fd_accident" - case .camera: - return "fd_camera" - case .traffic: - return "fd_incorrect_traffic" - case .wrongSpeedLimit: - return "fd_wrong_speed_limit" - } - } - - public var subtypeKey: String? { - return nil - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/SearchFeedbackType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Feedback/SearchFeedbackType.swift deleted file mode 100644 index ae2b0faec..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Feedback/SearchFeedbackType.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -@_documentation(visibility: internal) -public enum SearchFeedbackType: FeedbackType { - case incorrectName - case incorrectAddress - case incorrectLocation - case phoneNumber - case resultRank - case missingResult - case other - - public var typeKey: String { - switch self { - case .missingResult: - return "cannot_find" - case .incorrectName: - return "incorrect_name" - case .incorrectAddress: - return "incorrect_address" - case .incorrectLocation: - return "incorrect_location" - case .other: - return "other_result_issue" - case .phoneNumber: - return "incorrect_phone_number" - case .resultRank: - return "incorrect_result_rank" - } - } - - public var subtypeKey: String? { - return nil - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/AttachmentsUploader.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/AttachmentsUploader.swift deleted file mode 100644 index 1d2b5320e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/AttachmentsUploader.swift +++ /dev/null @@ -1,152 +0,0 @@ -import Foundation -import MapboxCommon - -struct AttachmentArchive { - struct FileType { - var format: String - var type: String - - static var gzip: Self { .init(format: "gz", type: "zip") } - } - - var fileUrl: URL - var fileName: String - var fileId: String - var sessionId: String - var fileType: FileType - var createdAt: Date -} - -protocol AttachmentsUploader { - func upload(accessToken: String, archive: AttachmentArchive) async throws -} - -final class AttachmentsUploaderImpl: AttachmentsUploader { - private enum Constants { -#if DEBUG - static let baseUploadURL = "https://api-events-staging.tilestream.net" -#else - static let baseUploadURL = "https://events.mapbox.com" -#endif - static let mediaTypeZip = "application/zip" - } - - private let dateFormatter: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [ - .withInternetDateTime, - .withFractionalSeconds, - .withFullTime, - .withDashSeparatorInDate, - .withColonSeparatorInTime, - .withColonSeparatorInTimeZone, - ] - return formatter - }() - - let options: MapboxCopilot.Options - var sdkInformation: SdkInformation { - options.sdkInformation - } - - private var _filesDir: URL? - private let lock = NSLock() - private var filesDir: URL { - lock.withLock { - if let url = _filesDir { - return url - } - - let cacheDir = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first - ?? NSTemporaryDirectory() - let url = URL(fileURLWithPath: cacheDir, isDirectory: true) - .appendingPathComponent("NavigationHistoryAttachments") - if FileManager.default.fileExists(atPath: url.path) == false { - try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) - } - _filesDir = url - return url - } - } - - init(options: MapboxCopilot.Options) { - self.options = options - } - - func upload(accessToken: String, archive: AttachmentArchive) async throws { - let metadata: [String: String] = [ - "name": archive.fileName, - "fileId": archive.fileId, - "sessionId": archive.sessionId, - "format": archive.fileType.format, - "created": dateFormatter.string(from: archive.createdAt), - "type": archive.fileType.type, - ] - let filePath = try prepareFileUpload(of: archive) - let jsonData = (try? JSONEncoder().encode([metadata])) ?? Data() - let jsonString = String(data: jsonData, encoding: .utf8) ?? "" - let log = options.log - return try await withCheckedThrowingContinuation { continuation in - do { - try HttpServiceFactory.getInstance().upload(for: .init( - filePath: filePath, - url: uploadURL(accessToken), - headers: [:], - metadata: jsonString, - mediaType: Constants.mediaTypeZip, - sdkInformation: sdkInformation - )) { [weak self] status in - switch status.state { - case .failed: - let errorMessage = status.error?.message ?? "Unknown upload error" - let error = CopilotError( - errorType: .failedToUploadHistoryFile, - userInfo: ["errorMessage": errorMessage] - ) - log?("Failed to upload session to attachements. \(errorMessage)") - try? self?.cleanupFileUpload(of: archive, from: filePath) - continuation.resume(throwing: error) - case .finished: - try? self?.cleanupFileUpload(of: archive, from: filePath) - continuation.resume() - case .pending, .inProgress: - break - @unknown default: - break - } - } - } catch { - continuation.resume(throwing: error) - } - } - } - - private func prepareFileUpload(of archive: AttachmentArchive) throws -> String { - guard archive.fileUrl.lastPathComponent != archive.fileName else { - return archive.fileUrl.path - } - let fileManager = FileManager.default - let tmpPath = filesDir.appendingPathComponent(archive.fileName).path - - if fileManager.fileExists(atPath: tmpPath) { - try fileManager.removeItem(atPath: tmpPath) - } - - try fileManager.copyItem( - atPath: archive.fileUrl.path, - toPath: tmpPath - ) - return tmpPath - } - - private func cleanupFileUpload(of archive: AttachmentArchive, from temporaryPath: String) throws { - guard archive.fileUrl.path != temporaryPath else { - return - } - try FileManager.default.removeItem(atPath: temporaryPath) - } - - private func uploadURL(_ accessToken: String) throws -> String { - return Constants.baseUploadURL + "/attachments/v1?access_token=\(accessToken)" - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/CopilotService.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/CopilotService.swift deleted file mode 100644 index b8ce5ef06..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/CopilotService.swift +++ /dev/null @@ -1,99 +0,0 @@ -import Foundation -import MapboxNavigationNative -import UIKit - -public actor CopilotService { - private final class HistoryProviderAdapter: NavigationHistoryProviderProtocol, @unchecked Sendable { - private let historyRecording: HistoryRecording - - init(_ historyRecording: HistoryRecording) { - self.historyRecording = historyRecording - } - - func startRecording() { - historyRecording.startRecordingHistory() - } - - func pushEvent(event: some NavigationHistoryEvent) throws { - try historyRecording.pushHistoryEvent(type: event.eventType, value: event.payload) - } - - func dumpHistory(_ completion: @escaping @Sendable (DumpResult) -> Void) { - historyRecording.stopRecordingHistory(writingFileWith: { url in - guard let url else { - completion(.failure(.noHistory)) - return - } - completion(.success((url.absoluteString, .protobuf))) - }) - } - } - - public private(set) var mapboxCopilot: MapboxCopilot? - - public func setActive(_ isActive: Bool) { - self.isActive = isActive - } - - public private(set) var isActive: Bool { - get { - mapboxCopilot != nil - } - set { - switch (newValue, mapboxCopilot) { - case (true, .none): - activateCopilot() - case (false, .some): - mapboxCopilot = nil - default: - break - } - } - } - - private let accessToken: String - private let navNativeVersion: String - private let historyRecording: HistoryRecording - private let log: (@Sendable (String) -> Void)? - public func setDelegate(_ delegate: MapboxCopilotDelegate) { - self.delegate = delegate - } - - public private(set) weak var delegate: MapboxCopilotDelegate? - - private func activateCopilot() { - Task { - mapboxCopilot = await MapboxCopilot( - options: MapboxCopilot.Options( - accessToken: accessToken, - userId: UIDevice.current.identifierForVendor?.uuidString ?? "-", - navNativeVersion: navNativeVersion, - sdkVersion: Bundle.mapboxNavigationVersion, - sdkName: Bundle.resolvedNavigationSDKName, - packageName: Bundle.mapboxNavigationUXBundleIdentifier, - log: log - ), - historyProvider: HistoryProviderAdapter(historyRecording) - ) - await mapboxCopilot?.setDelegate(delegate) - } - } - - public init( - accessToken: String, - navNativeVersion: String, - historyRecording: HistoryRecording, - isActive: Bool = true, - log: (@Sendable (String) -> Void)? = nil - ) { - self.accessToken = accessToken - self.navNativeVersion = navNativeVersion - self.historyRecording = historyRecording - self.log = log - defer { - Task { - await self.setActive(isActive) - } - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/ApplicationState.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/ApplicationState.swift deleted file mode 100644 index 2030ec5dc..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/ApplicationState.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -extension NavigationHistoryEvents { - enum ApplicationState { - case goingToBackground - case goingToForeground - } -} - -extension NavigationHistoryEvents.ApplicationState: NavigationHistoryEvents.Event { - var eventType: String { - switch self { - case .goingToBackground: - return "going_to_background" - case .goingToForeground: - return "going_to_foreground" - } - } - - var payload: String? { nil } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/DriveEnds.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/DriveEnds.swift deleted file mode 100644 index 343789d43..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/DriveEnds.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -extension NavigationHistoryEvents { - struct DriveEnds: Event { - enum DriveEndType: String, Encodable { - case applicationClosed = "application_closed" - case vehicleParked = "vehicle_parked" - case arrived - case canceledManually = "canceled_manually" - } - - struct Payload: Encodable { - var type: DriveEndType - var realDuration: Int - } - - let eventType = "drive_ends" - var payload: Payload - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/InitRoute.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/InitRoute.swift deleted file mode 100644 index 37bfd72e3..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/InitRoute.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation - -extension NavigationHistoryEvents { - struct InitRoute: Event { - struct Payload: Encodable { - var requestIdentifier: String? - var route: String - } - - let eventType = "init_route" - let payload: Payload - - init?( - requestIdentifier: String?, - route: Encodable - ) { - let encoder = JSONEncoder() - guard let encodedData = try? encoder.encode(route), - let encodedRoute = String(data: encodedData, encoding: .utf8) - else { - assertionFailure("No route") - return nil - } - self.payload = .init( - requestIdentifier: requestIdentifier, - route: encodedRoute - ) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationFeedback.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationFeedback.swift deleted file mode 100644 index 709d17d0a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationFeedback.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -extension NavigationHistoryEvents { - struct NavigationFeedback: Event { - struct Payload: Encodable { - var feedbackId: String - var type: String - var subtype: [String] - var coordinate: Coordinate - } - - let eventType = "nav_feedback_submitted" - var payload: Payload - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationHistoryEvent.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationHistoryEvent.swift deleted file mode 100644 index c79ef8052..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/NavigationHistoryEvent.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation - -public protocol NavigationHistoryEvent { - associatedtype Payload: Encodable - - var eventType: String { get } - var payload: Payload { get } -} - -public enum NavigationHistoryEvents { - typealias Event = NavigationHistoryEvent -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResultUsed.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResultUsed.swift deleted file mode 100644 index 98b8a4691..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResultUsed.swift +++ /dev/null @@ -1,62 +0,0 @@ -import CoreLocation -import Foundation - -extension NavigationHistoryEvents { - public struct Coordinate: Encodable, Sendable { - var latitude: Double - var longitude: Double - - public init(_ coordinate: CLLocationCoordinate2D) { - self.latitude = coordinate.latitude - self.longitude = coordinate.longitude - } - } - - public struct RoutablePoint: Encodable, Sendable { - public var coordinates: Coordinate - - public init(coordinates: Coordinate) { - self.coordinates = coordinates - } - } - - public struct SearchResultUsed: Event, Sendable { - public enum Provider: String, Encodable, Sendable { - case mapbox - } - - public struct Payload: Encodable, Sendable { - public var provider: Provider - public var id: String - - public var name: String - public var address: String - public var coordinate: Coordinate - - public var routablePoint: [RoutablePoint]? - - public init( - provider: Provider, - id: String, - name: String, - address: String, - coordinate: Coordinate, - routablePoint: [RoutablePoint]? - ) { - self.provider = provider - self.id = id - self.name = name - self.address = address - self.coordinate = coordinate - self.routablePoint = routablePoint - } - } - - public let eventType = "search_result_used" - public var payload: Payload - - public init(payload: Payload) { - self.payload = payload - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResults.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResults.swift deleted file mode 100644 index 59a4b31b4..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Events/SearchResults.swift +++ /dev/null @@ -1,60 +0,0 @@ -import CoreLocation -import Foundation - -extension NavigationHistoryEvents { - public struct SearchResults: Event, Sendable { - public struct SearchResult: Encodable, Sendable { - public var id: String - public var name: String - public var address: String - public var coordinate: Coordinate? - public var routablePoint: [RoutablePoint]? - - public init( - id: String, - name: String, - address: String, - coordinate: NavigationHistoryEvents.Coordinate? = nil, - routablePoint: [NavigationHistoryEvents.RoutablePoint]? = nil - ) { - self.id = id - self.name = name - self.address = address - self.coordinate = coordinate - self.routablePoint = routablePoint - } - } - - public struct Payload: Encodable, Sendable { - public var provider: SearchResultUsed.Provider - public var request: String - public var response: String? - public var error: String? - public var searchQuery: String - public var results: [SearchResult]? - - public init( - provider: NavigationHistoryEvents.SearchResultUsed.Provider, - request: String, - response: String? = nil, - error: String? = nil, - searchQuery: String, - results: [NavigationHistoryEvents.SearchResults.SearchResult]? = nil - ) { - self.provider = provider - self.request = request - self.response = response - self.error = error - self.searchQuery = searchQuery - self.results = results - } - } - - public let eventType = "search_results" - public var payload: Payload - - public init(payload: Payload) { - self.payload = payload - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/FeedbackEventsObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/FeedbackEventsObserver.swift deleted file mode 100644 index 6ed86733c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/FeedbackEventsObserver.swift +++ /dev/null @@ -1,99 +0,0 @@ -import Combine -import Foundation -import MapboxCommon_Private - -protocol FeedbackEventsObserver { - var navigationFeedbackPublisher: AnyPublisher { get } - - func refreshSubscription() -} - -final class FeedbackEventsObserverImpl: NSObject, FeedbackEventsObserver { - private let navigationFeedbackSubject = PassthroughSubject() - var navigationFeedbackPublisher: AnyPublisher { - navigationFeedbackSubject.eraseToAnyPublisher() - } - - private var eventsAPI: EventsService? - private let options: MapboxCopilot.Options - private var log: MapboxCopilot.Log? { - options.log - } - - init(options: MapboxCopilot.Options) { - self.options = options - - super.init() - refreshSubscription() - } - - func refreshSubscription() { - let sdkInformation = options.feedbackEventsSdkInformation - let options = EventsServerOptions( - sdkInformation: sdkInformation, - deferredDeliveryServiceOptions: nil - ) - eventsAPI = EventsService.getOrCreate(for: options) - eventsAPI?.registerObserver(for: self) - } - - private func parseEvent(_ attributes: [String: Any]) { - switch attributes["event"] as? String { - case "navigation.feedback": - parseNavigationFeedbackEvent(attributes) - default: - log?("Skipping unknown event with attributes: \(attributes)") - } - } - - private func parseNavigationFeedbackEvent(_ attributes: [String: Any]) { - guard let rawFeedbackId = attributes["feedbackId"], - let rawFeedbackType = attributes["feedbackType"], - let rawLatitude = attributes["lat"], - let rawLongitude = attributes["lng"] - else { - assertionFailure("Failed to parse navigation feedback event") - log?("Failed to fetch required fields for navigation feedback event") - return - } - - do { - let typeConverter = TypeConverter() - let feedbackId = try typeConverter.convert(from: rawFeedbackId, to: String.self) - let feedbackType = try typeConverter.convert(from: rawFeedbackType, to: String.self) - let latitude = try typeConverter.convert(from: rawLatitude, to: Double.self) - let longitude = try typeConverter.convert(from: rawLongitude, to: Double.self) - let feedbackSubtype: [String] = try attributes["feedbackSubType"].map { - try typeConverter.convert(from: $0, to: [String].self) - } ?? [] - let event = NavigationHistoryEvents.NavigationFeedback( - payload: .init( - feedbackId: feedbackId, - type: feedbackType, - subtype: feedbackSubtype, - coordinate: .init(.init(latitude: latitude, longitude: longitude)) - ) - ) - navigationFeedbackSubject.send(event) - } catch { - assertionFailure("Failed to parse navigation feedback event") - } - } -} - -extension FeedbackEventsObserverImpl: EventsServiceObserver { - func didEncounterError(forError error: EventsServiceError, events: Any) { - let eventsDescription = (events as? [[String: Any]])?.description ?? "" - log?("Events Service did encounter error: \(error.message). Events: \(eventsDescription)") - } - - func didSendEvents(forEvents events: Any) { - guard let events = events as? [[String: Any]] else { - assertionFailure("Failed to parse navigation feedback event.") - return - } - for event in events { - parseEvent(event) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilot.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilot.swift deleted file mode 100644 index 751f30b60..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilot.swift +++ /dev/null @@ -1,207 +0,0 @@ -import Foundation -import MapboxCommon -import UIKit - -public actor MapboxCopilot { - public typealias Log = @Sendable (String) -> Void - public struct Options: Sendable { - var accessToken: String - var userId: String = UUID().uuidString - var navNativeVersion: String - var sdkVersion: String - var sdkName: String - var packageName: String - var log: (@Sendable (String) -> Void)? - - var sdkInformation: SdkInformation { - .init( - name: sdkName, - version: sdkVersion, - packageName: packageName - ) - } - - var feedbackEventsSdkInformation: SdkInformation { - .init( - name: "MapboxNavigationNative", - version: navNativeVersion, - packageName: nil - ) - } - - public init( - accessToken: String, - userId: String, - navNativeVersion: String, - sdkVersion: String, - sdkName: String, - packageName: String, - log: (@Sendable (String) -> Void)? = nil - ) { - self.accessToken = accessToken - self.userId = userId - self.navNativeVersion = navNativeVersion - self.sdkVersion = sdkVersion - self.sdkName = sdkName - self.log = log - self.packageName = packageName - } - } - - private let eventsController: NavigationHistoryEventsController - private let manager: NavigationHistoryManager - private let historyProvider: NavigationHistoryProviderProtocol - private let options: Options - - public private(set) var currentSession: NavigationSession? - - public func setDelegate(_ delegate: MapboxCopilotDelegate?) { - self.delegate = delegate - } - - public private(set) weak var delegate: MapboxCopilotDelegate? - - @MainActor - public init( - options: Options, - historyProvider: NavigationHistoryProviderProtocol - ) { - self.init( - options: options, - manager: NavigationHistoryManager( - options: options - ), - historyProvider: historyProvider, - eventsController: NavigationHistoryEventsControllerImpl( - historyProvider: historyProvider, - options: options - ) - ) - } - - init( - options: Options, - manager: NavigationHistoryManager, - historyProvider: NavigationHistoryProviderProtocol, - eventsController: NavigationHistoryEventsController - ) { - self.options = options - self.manager = manager - self.historyProvider = historyProvider - self.eventsController = eventsController - - manager.delegate = self - } - - @discardableResult - public func startActiveGuidanceSession( - requestIdentifier: String?, - route: Encodable, - searchResultUsed: NavigationHistoryEvents.SearchResultUsed? = nil - ) throws -> String { - let session = NavigationSession( - sessionType: .activeGuidance, - accessToken: options.accessToken, - userId: options.userId, - routeId: requestIdentifier, - navNativeVersion: options.navNativeVersion, - navigationVersion: options.sdkVersion - ) - try startSession(session) - try eventsController.startActiveGuidanceSession( - requestIdentifier: requestIdentifier, - route: route, - searchResultUsed: searchResultUsed - ) - return session.id - } - - @discardableResult - public func startFreeDriveSession() throws -> String { - let session = NavigationSession( - sessionType: .freeDrive, - accessToken: options.accessToken, - userId: options.userId, - routeId: nil, - navNativeVersion: options.navNativeVersion, - navigationVersion: options.sdkVersion - ) - try startSession(session) - eventsController.startFreeDriveSession() - return session.id - } - - private func startSession(_ session: NavigationSession) throws { - try completeNavigationSession() - currentSession = session - manager.update(session) - historyProvider.startRecording() - } - - public func arrive() { - eventsController.arrive() - } - - public func completeNavigationSession() throws { - guard var currentSession else { return } - self.currentSession = nil - - currentSession.endedAt = Date() - manager.update(currentSession) - try eventsController.completeSession() - let immutableSession = currentSession - - historyProvider.dumpHistory { [weak self] dump in - Task.detached { [self, immutableSession] in - guard let self else { return } - var currentSession = immutableSession - await self.updateSession(¤tSession, with: dump) - await self.delegate?.copilot(self, didFinishRecording: currentSession) - await self.manager.complete(currentSession) - } - } - } - - public func reportSearchResults(_ event: NavigationHistoryEvents.SearchResults) throws { - try eventsController.reportSearchResults(event) - } - - private func updateSession( - _ session: inout NavigationSession, - with result: NavigationHistoryProviderProtocol.DumpResult - ) { - var format: NavigationHistoryFormat? - var errorString: String? - var fileName: String? - switch result { - case .success(let result): - (fileName, format) = result - case .failure(.noHistory): - errorString = "NN provided no history" - delegate?.copilot(self, didEncounterError: .history(.noHistoryFileProvided, session: session)) - case .failure(.notFound(let path)): - errorString = "History file provided by NN is not found at '\(path)'" - delegate?.copilot(self, didEncounterError: .history(.notFound, session: session, userInfo: ["path": path])) - } - session.historyFormat = format - session.historyError = errorString - session.lastHistoryFileName = fileName - } -} - -extension MapboxCopilot: NavigationHistoryManagerDelegate { - nonisolated func historyManager( - _ historyManager: NavigationHistoryManager, - didUploadHistoryForSession session: NavigationSession - ) { - Task { - await delegate?.copilot(self, didUploadHistoryFileForSession: session) - } - } - - nonisolated func historyManager(_ historyManager: NavigationHistoryManager, didEncounterError error: CopilotError) { - Task { - await delegate?.copilot(self, didEncounterError: error) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilotDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilotDelegate.swift deleted file mode 100644 index dcf159849..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/MapboxCopilotDelegate.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -public protocol MapboxCopilotDelegate: AnyObject, Sendable { - func copilot(_ copilot: MapboxCopilot, didFinishRecording session: NavigationSession) - func copilot(_ copilot: MapboxCopilot, didUploadHistoryFileForSession session: NavigationSession) - func copilot(_ copilot: MapboxCopilot, didEncounterError error: CopilotError) -} - -/// Default implementations do nothing -extension MapboxCopilotDelegate { - func copilot(_ copilot: MapboxCopilot, didFinishRecording session: NavigationSession) {} - func copilot(_ copilot: MapboxCopilot, didUploadHistoryFileForSession session: NavigationSession) {} - func copilot(_ copilot: MapboxCopilot, didEncounterError error: CopilotError) {} -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryAttachmentProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryAttachmentProvider.swift deleted file mode 100644 index e65368193..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryAttachmentProvider.swift +++ /dev/null @@ -1,132 +0,0 @@ -import Foundation - -enum NavigationHistoryAttachmentProvider { - enum Error: Swift.Error { - case noFile - case unsupportedFormat - case noTokenOwner - } - - static func attachementArchive(for session: NavigationSession) throws -> AttachmentArchive { - guard let fileUrl = session.lastHistoryFileUrl else { throw Error.noFile } - - return try .init( - fileUrl: fileUrl, - fileName: session.attachmentFileName(), - fileId: UUID().uuidString, - sessionId: session.attachmentSessionId(), - fileType: .gzip, - createdAt: session.startedAt - ) - } -} - -extension NavigationSession { - var formattedStartedAt: String { - startedAt.metadataValue - } - - var formattedEndedAt: String? { - endedAt?.metadataValue - } -} - -extension NavigationSession { - private typealias Error = NavigationHistoryAttachmentProvider.Error - - fileprivate func attachmentFileName() throws -> String { - guard let historyFormat else { throw Error.unsupportedFormat } - - return Self.composeParts(fallback: "_", separator: "__", escape: ["_", "/"], parts: [ - /* log-start-date */ startedAt.metadataValue, - /* log-end-date */ endedAt?.metadataValue, - /* sdk-platform */ "ios", - /* nav-sdk-version */ navigationSdkVersion, - /* nav-native-sdk-version */ navigationNativeSdkVersion, - /* nav-session-id */ nil, - /* app-version */ appVersion, - /* app-user-id */ userId, - /* app-session-id */ appSessionId, - ]) + ".\(historyFormat.fileExtension)" - } - - fileprivate func attachmentSessionId() throws -> String { - guard let owner = tokenOwner else { throw Error.noTokenOwner } - - return Self.composeParts(fallback: "-", separator: "/", parts: [ - /* unique-prefix */ "co-pilot", owner, - // We can use 1.1 for 1.0 as there are only changes to file name and session id - /* specification-version */ "1.1", - /* app-mode */ appMode, - /* dt */ nil, - /* hr */ nil, - /* drive-mode */ sessionType.metadataValue, - /* telemetry-user-id */ nil, - /* drive-id */ id, - ]) - } - - private static func composeParts( - fallback: String, - separator: String, - escape: [Character] = [], - parts: [String?] - ) -> String { - parts - .map { part in - part.map { escapePart(part: $0, charsToEscape: escape) } - } - .map { $0 ?? fallback } - .joined(separator: separator) - } - - /// Escape characters from `charsToEscape` by prefixing `\` to them - private static func escapePart(part: String, charsToEscape: [Character]) -> String { - charsToEscape.reduce(part) { - $0.replacingOccurrences(of: "\($1)", with: "\\\($1)") - } - } -} - -extension Date { - fileprivate static let metadataFormatter: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [ - .withInternetDateTime, - .withFractionalSeconds, - .withFullTime, - .withDashSeparatorInDate, - .withColonSeparatorInTime, - .withColonSeparatorInTimeZone, - ] - return formatter - }() - - fileprivate var metadataValue: String { - Self.metadataFormatter.string(from: self) - } -} - -extension NavigationHistoryFormat { - fileprivate var fileExtension: String { - switch self { - case .json: - return "json" - case .protobuf: - return "pbf.gz" - case .unknown(let ext): - return ext - } - } -} - -extension NavigationSession.SessionType { - var metadataValue: String { - switch self { - case .activeGuidance: - return "active-guidance" - case .freeDrive: - return "free-drive" - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryErrorReport.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryErrorReport.swift deleted file mode 100644 index 2d566f2c4..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryErrorReport.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -public struct CopilotError: Error { - enum CopilotErrorType: String { - case noHistoryFileProvided - case notFound - case missingLastHistoryFile - case failedAttachmentsUpload - case failedToUploadHistoryFile - case failedToFetchAccessToken - } - - var errorType: CopilotErrorType - var userInfo: [String: String?]? -} - -extension CopilotError { - static func history( - _ errorType: CopilotErrorType, - session: NavigationSession? = nil, - userInfo: [String: String?]? = nil - ) -> Self { - var userInfo = userInfo - if let session { - var extendedUserInfo = userInfo ?? [:] - extendedUserInfo["sessionId"] = session.id - extendedUserInfo["sessionType"] = session.sessionType.rawValue - if let endedAt = session.endedAt { - extendedUserInfo["duration"] = "\(Int(endedAt.timeIntervalSince(session.startedAt)))" - } - userInfo = extendedUserInfo - } - return Self(errorType: errorType, userInfo: userInfo) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryEventsController.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryEventsController.swift deleted file mode 100644 index 53a65391a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryEventsController.swift +++ /dev/null @@ -1,125 +0,0 @@ -import Combine -import Foundation -import UIKit - -protocol NavigationHistoryEventsController { - func startActiveGuidanceSession( - requestIdentifier: String?, - route: Encodable, - searchResultUsed: NavigationHistoryEvents.SearchResultUsed? - ) throws - func startFreeDriveSession() - func arrive() - func completeSession() throws - - func reportSearchResults(_ event: NavigationHistoryEvents.SearchResults) throws - func resetSearchResults() -} - -final class NavigationHistoryEventsControllerImpl: NavigationHistoryEventsController { - private let historyProvider: NavigationHistoryProviderProtocol - private let feedbackEventsObserver: FeedbackEventsObserver - private let timeProvider: () -> TimeInterval - - private var sessionStartTimestamp: TimeInterval? - private var arrived = false - private var lastSearchResultsEvent: NavigationHistoryEvents.SearchResults? - - private var lifetimeSubscriptions = Set() - - @MainActor - init( - historyProvider: NavigationHistoryProviderProtocol, - options: MapboxCopilot.Options, - feedbackEventsObserver: FeedbackEventsObserver? = nil, - timeProvider: @escaping () -> TimeInterval = { ProcessInfo.processInfo.systemUptime } - ) { - self.historyProvider = historyProvider - self.feedbackEventsObserver = feedbackEventsObserver ?? FeedbackEventsObserverImpl( - options: options - ) - self.timeProvider = timeProvider - - NotificationCenter.default.addObserver( - self, - selector: #selector(applicationDidEnterBackground), - name: UIApplication.didEnterBackgroundNotification, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(applicationWillEnterForeground), - name: UIApplication.willEnterForegroundNotification, - object: nil - ) - - self.feedbackEventsObserver.navigationFeedbackPublisher - .receive(on: DispatchQueue.main) - .sink { - try? historyProvider.pushEvent(event: $0) - } - .store(in: &lifetimeSubscriptions) - } - - func startActiveGuidanceSession( - requestIdentifier: String?, - route: Encodable, - searchResultUsed: NavigationHistoryEvents.SearchResultUsed? - ) throws { - resetSession() - if let event = NavigationHistoryEvents.InitRoute( - requestIdentifier: requestIdentifier, - route: route - ) { - try? historyProvider.pushEvent(event: event) - } - try lastSearchResultsEvent.flatMap { try historyProvider.pushEvent(event: $0) } - try searchResultUsed.flatMap { try historyProvider.pushEvent(event: $0) } - } - - func startFreeDriveSession() { - resetSession() - } - - private func resetSession() { - sessionStartTimestamp = timeProvider() - arrived = false - feedbackEventsObserver.refreshSubscription() - } - - func arrive() { - arrived = true - } - - func completeSession() throws { - if let sessionStartTimestamp { - let duration = timeProvider() - sessionStartTimestamp - self.sessionStartTimestamp = nil - try historyProvider.pushEvent(event: NavigationHistoryEvents.DriveEnds(payload: .init( - type: arrived ? .arrived : .canceledManually, - realDuration: Int(duration * 1e3) - ))) - } - } - - func reportSearchResults(_ event: NavigationHistoryEvents.SearchResults) throws { - lastSearchResultsEvent = event - try historyProvider.pushEvent(event: event) - } - - func resetSearchResults() { - lastSearchResultsEvent = nil - } - - // MARK: - Notifications - - @objc - private func applicationDidEnterBackground() { - try? historyProvider.pushEvent(event: NavigationHistoryEvents.ApplicationState.goingToBackground) - } - - @objc - private func applicationWillEnterForeground() { - try? historyProvider.pushEvent(event: NavigationHistoryEvents.ApplicationState.goingToForeground) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryFormat.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryFormat.swift deleted file mode 100644 index b0ce50a09..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryFormat.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -public enum NavigationHistoryFormat: Codable, Equatable, Sendable { - case json - case protobuf - case unknown(String) - - private static let jsonExt = "json" - private static let protobufExt = "pbf.gz" - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let value = try container.decode(String.self) - switch value { - case Self.jsonExt: - self = .json - case Self.protobufExt: - self = .protobuf - default: - self = .unknown(value) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - let value: String = switch self { - case .json: - Self.jsonExt - case .protobuf: - Self.protobufExt - case .unknown(let ext): - ext - } - try container.encode(value) - } - - public static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case (.json, .json), (.protobuf, .protobuf): - return true - case (.unknown(let lhsExt), .unknown(let rhsExt)): - return lhsExt == rhsExt - default: - return false - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryLocalStorage.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryLocalStorage.swift deleted file mode 100644 index 5194c48e4..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryLocalStorage.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation - -protocol NavigationHistoryLocalStorageProtocol: Sendable { - func savedSessions() -> [NavigationSession] - func saveSession(_ session: NavigationSession) - func deleteSession(_ session: NavigationSession) -} - -/// Stores history files metadata -final class NavigationHistoryLocalStorage: NavigationHistoryLocalStorageProtocol, @unchecked Sendable { - private static let storageUrl = FileManager.applicationSupportURL - .appendingPathComponent("com.mapbox.Copilot") - .appendingPathComponent("NavigationSessions") - - private let log: MapboxCopilot.Log? - - init(log: MapboxCopilot.Log?) { - self.log = log - } - - func savedSessions() -> [NavigationSession] { - guard let enumerator = FileManager.default.enumerator(at: Self.storageUrl, includingPropertiesForKeys: nil) - else { - return [] - } - - let decoder = JSONDecoder() - let fileUrls = enumerator.compactMap { (element: NSEnumerator.Element) -> URL? in element as? URL } - - var sessions = [NavigationSession]() - for fileUrl in fileUrls { - do { - let data = try Data(contentsOf: fileUrl) - let session = try decoder.decode(NavigationSession.self, from: data) - sessions.append(session) - } catch { - log?("Failed to decode navigation session. Error: \(error). Path: \(fileUrl.absoluteString)") - try? FileManager.default.removeItem(at: fileUrl) - } - } - return sessions - } - - func saveSession(_ session: NavigationSession) { - let fileUrl = storageFileUrl(for: session) - do { - let data = try JSONEncoder().encode(session) - let parentDirectory = fileUrl.deletingLastPathComponent() - if FileManager.default.fileExists(atPath: parentDirectory.path) == false { - try FileManager.default.createDirectory( - at: parentDirectory, - withIntermediateDirectories: true, - attributes: nil - ) - } - try data.write(to: fileUrl, options: .atomic) - } catch { - log?("Failed to save navigation session. Error: \(error). Session id: \(session.id)") - } - } - - func deleteSession(_ session: NavigationSession) { - do { - try FileManager.default.removeItem(at: storageFileUrl(for: session)) - try session.deleteLastHistoryFile() - } catch { - log?("Failed to delete navigation session. Error: \(error). Session id: \(session.id)") - } - } - - static func removeExpiredMetadataFiles(deadline: Date) { - FileManager.default.removeFiles(in: storageUrl, createdBefore: deadline) - } - - private func storageFileUrl(for session: NavigationSession) -> URL { - return Self.storageUrl.appendingPathComponent(session.id).appendingPathExtension("json") - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryManager.swift deleted file mode 100644 index 9729d7613..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryManager.swift +++ /dev/null @@ -1,115 +0,0 @@ -import Combine -import Foundation - -protocol NavigationHistoryManagerDelegate: AnyObject { - func historyManager(_ historyManager: NavigationHistoryManager, didEncounterError error: CopilotError) - func historyManager( - _ historyManager: NavigationHistoryManager, - didUploadHistoryForSession session: NavigationSession - ) -} - -final class NavigationHistoryManager: ObservableObject, @unchecked Sendable { - private enum RemovalPolicy { - static let maxTimeIntervalToKeepHistory: TimeInterval = -24 * 60 * 60 // 1 day - } - - private let localStorage: NavigationHistoryLocalStorageProtocol? - private let uploader: NavigationHistoryUploaderProtocol - private let log: MapboxCopilot.Log? - - weak var delegate: NavigationHistoryManagerDelegate? - - convenience init(options: MapboxCopilot.Options) { - self.init( - localStorage: NavigationHistoryLocalStorage(log: options.log), - uploader: NavigationHistoryUploader(options: options), - log: options.log - ) - } - - init( - localStorage: NavigationHistoryLocalStorageProtocol?, - uploader: NavigationHistoryUploaderProtocol, - log: MapboxCopilot.Log? - ) { - self.localStorage = localStorage - self.uploader = uploader - self.log = log - - loadAndUploadPreviousSessions() - } - - func loadAndUploadPreviousSessions() { - guard let localStorage else { return } - Task.detached { [weak self] in - guard let self else { return } - let restoredSessions = localStorage.savedSessions() - .filter { session in - if self.shouldRetryUpload(session) { - return true - } else { - localStorage.deleteSession(session) - return false - } - } - .sorted(by: { $0.startedAt > $1.startedAt }) - - let removalDeadline = Date().addingTimeInterval(RemovalPolicy.maxTimeIntervalToKeepHistory) - NavigationHistoryLocalStorage.removeExpiredMetadataFiles(deadline: removalDeadline) - for session in restoredSessions { - await upload(session) - } - } - } - - func complete(_ session: NavigationSession) async { - var session = session - session.state = .local - localStorage?.saveSession(session) - await upload(session) - } - - func update(_ session: NavigationSession) { - localStorage?.saveSession(session) - } - - private func upload(_ session: NavigationSession) async { - var session = session - session.state = .uploading - - do { - try await uploader.upload(session, log: log) - delegate?.historyManager(self, didUploadHistoryForSession: session) - localStorage?.deleteSession(session) - } catch { - // We will retry to upload the file on next launch - session.state = .local - delegate?.historyManager(self, didEncounterError: .history( - .failedAttachmentsUpload, - session: session, - userInfo: ["error": error.localizedDescription] - )) - localStorage?.saveSession(session) - } - } - - private func shouldRetryUpload(_ session: NavigationSession) -> Bool { - guard let url = session.lastHistoryFileUrl, FileManager.default.fileExists(atPath: url.path) else { - delegate?.historyManager(self, didEncounterError: .history(.missingLastHistoryFile, session: session)) - return false - } - guard session.startedAt.timeIntervalSinceNow < RemovalPolicy.maxTimeIntervalToKeepHistory else { - // File is too old to be uploaded, we will delete it - return false - } - switch session.state { - case .local, .uploading: - return true - case .inProgress: - // Session might be in `.inProgress` state if it wasn't finished properly (i.e. a crash happened) - // In this case we don't want to upload a potentially corrupted file - return false - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryProvider.swift deleted file mode 100644 index 78e5ef758..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryProvider.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -public enum NavigationHistoryProviderError: Error, Sendable { - case noHistory - case notFound(_ path: String) -} - -public protocol NavigationHistoryProviderProtocol: AnyObject { - typealias Filepath = String - typealias DumpResult = Result<(Filepath, NavigationHistoryFormat), NavigationHistoryProviderError> - - func startRecording() - func pushEvent(event: T) throws - func dumpHistory(_ completion: @escaping @Sendable (DumpResult) -> Void) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryUploader.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryUploader.swift deleted file mode 100644 index 0b97cc25f..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationHistoryUploader.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Foundation -import UIKit - -protocol NavigationHistoryUploaderProtocol { - func upload(_: NavigationSession, log: MapboxCopilot.Log?) async throws -} - -final class NavigationHistoryUploader: NavigationHistoryUploaderProtocol { - private let attachmentsUploader: AttachmentsUploaderImpl - - init(options: MapboxCopilot.Options) { - self.attachmentsUploader = AttachmentsUploaderImpl(options: options) - } - - @MainActor - func upload(_ session: NavigationSession, log: MapboxCopilot.Log?) async throws { - var backgroundTask: UIBackgroundTaskIdentifier? - let completeBackgroundTask = { - guard let guardedBackgroundTask = backgroundTask else { return } - Task { - UIApplication.shared.endBackgroundTask(guardedBackgroundTask) - } - backgroundTask = nil - } - backgroundTask = UIApplication.shared.beginBackgroundTask( - withName: "Uploading session", - expirationHandler: completeBackgroundTask - ) - defer { completeBackgroundTask() } - try await uploadWithinTask(session, log: log) - } - - @MainActor - private func uploadWithinTask(_ session: NavigationSession, log: MapboxCopilot.Log?) async throws { - do { - try await uploadToAttachments(session: session, log: log) - log?( - "History session uploaded. Type: \(session.sessionType.metadataValue)." + - "Session id: \(session.id)" - ) - } catch { - log?( - "Failed to upload session. Error: \(error). Session id: \(session.id). " + - "Start time: \(session.startedAt)" - ) - throw error - } - } - - @MainActor - private func uploadToAttachments(session: NavigationSession, log: MapboxCopilot.Log?) async throws { - let attachment: AttachmentArchive - do { - attachment = try NavigationHistoryAttachmentProvider.attachementArchive(for: session) - } catch { - log?("Incompatible attachments upload. Session id: \(session.id). Error: \(error)") - throw error - } - - do { - try await attachmentsUploader.upload(accessToken: session.accessToken, archive: attachment) - } catch { - log?( - "Failed to upload history to Attachments API. Error: \(error). Session id: \(session.id)" + - "Start time: \(session.startedAt)" - ) - throw error - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationSession.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationSession.swift deleted file mode 100644 index fbfac6a50..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/NavigationSession.swift +++ /dev/null @@ -1,75 +0,0 @@ -import CoreLocation -import Foundation - -public struct NavigationSession: Codable, Equatable, @unchecked Sendable { - public enum SessionType: String, Codable { - case activeGuidance = "active_guidance" - case freeDrive = "free_drive" - } - - public enum State: String, Codable { - case inProgress = "in_progress" - case local - case uploading - } - - public let id: String - public let startedAt: Date - public let userId: String - public internal(set) var sessionType: SessionType - public internal(set) var accessToken: String - public internal(set) var state: State - public internal(set) var routeId: String? - public internal(set) var endedAt: Date? - public internal(set) var historyError: String? - public internal(set) var appMode: String - public internal(set) var appVersion: String - public internal(set) var navigationSdkVersion: String - public internal(set) var navigationNativeSdkVersion: String - public internal(set) var tokenOwner: String? - public internal(set) var appSessionId: String - var lastHistoryFileName: String? - var historyFormat: NavigationHistoryFormat? - - init( - sessionType: SessionType, - accessToken: String, - userId: String, - routeId: String?, - navNativeVersion: String, - navigationVersion: String - ) { - self.id = UUID().uuidString - self.sessionType = sessionType - self.userId = userId - self.routeId = routeId - self.accessToken = accessToken - - self.startedAt = Date() - self.tokenOwner = TokenOwnerProvider.owner(of: accessToken) - self.appMode = AppEnvironment.applicationMode - self.appVersion = AppEnvironment.hostApplicationVersion() - self.navigationSdkVersion = navigationVersion - self.navigationNativeSdkVersion = navNativeVersion - self.state = .inProgress - self.appSessionId = AppEnvironment.applicationSessionId - } -} - -extension NavigationSession { - @_spi(MapboxInternal) public var _lastHistoryFileName: String? { lastHistoryFileName } -} - -extension NavigationSession { - var lastHistoryFileUrl: URL? { - guard let lastHistoryFileName, lastHistoryFileName.isEmpty == false else { - return nil - } - return URL(string: lastHistoryFileName) - } - - func deleteLastHistoryFile() throws { - guard let url = lastHistoryFileUrl else { return } - try FileManager.default.removeItem(at: url) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/AppEnvironment.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/AppEnvironment.swift deleted file mode 100644 index 8d8dfc31d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/AppEnvironment.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -enum AppEnvironment { - static var applicationMode: String { -#if DEBUG - return "mbx-debug" -#else - return "mbx-prod" -#endif - } - - static let applicationSessionId = UUID().uuidString // Unique per application launch -} - -// MARK: - Version resolving - -extension AppEnvironment { - private static let infoPlistShortVersionKey = "CFBundleShortVersionString" - - enum SDK { - case navigation - case navigationNative - } - - static func hostApplicationVersion() -> String { - Bundle.main.infoDictionary?[infoPlistShortVersionKey] as? String ?? "unknown" - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/FileManager++.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/FileManager++.swift deleted file mode 100644 index 5e196cb47..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/FileManager++.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -extension FileManager { - static let applicationSupportURL = FileManager.default - .urls(for: .applicationSupportDirectory, in: .userDomainMask) - .first! - - func removeFiles(in directory: URL, createdBefore deadline: Date) { - guard let enumerator = FileManager.default.enumerator( - at: directory, - includingPropertiesForKeys: [.addedToDirectoryDateKey] - ) else { return } - - enumerator - .compactMap { (element: NSEnumerator.Element) -> (url: URL, date: Date)? in - guard let url = element as? URL, - let resourceValues = try? url.resourceValues(forKeys: [.addedToDirectoryDateKey]), - let date = resourceValues.addedToDirectoryDate - else { return nil } - return (url: url, date: date) - } - .filter { $0.date < deadline } - .forEach { - try? FileManager.default.removeItem(atPath: $0.url.path) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TokenOwnerProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TokenOwnerProvider.swift deleted file mode 100644 index 951df3de8..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TokenOwnerProvider.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -enum TokenOwnerProvider { - private struct JWTPayload: Decodable { - var u: String - } - - static func owner(of token: String) -> String? { - guard let infoBase64String = token.split(separator: ".").dropFirst().first, - let infoData = base64Decode(String(infoBase64String)), - let info = try? JSONDecoder().decode(JWTPayload.self, from: infoData) - else { - assertionFailure("Failed to parse token.") - return nil - } - return info.u - } - - private static func base64Decode(_ value: String) -> Data? { - var base64 = value - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - - let length = base64.lengthOfBytes(using: .utf8) - let requiredLength = Int(4.0 * ceil(Double(length) / 4.0)) - let paddingLength = requiredLength - length - if paddingLength > 0 { - base64 += String(repeating: "=", count: paddingLength) - } - - return Data(base64Encoded: base64, options: .ignoreUnknownCharacters) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TypeConverter.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TypeConverter.swift deleted file mode 100644 index 28565c816..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/Copilot/Utils/TypeConverter.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -struct TypeConverter { - func convert( - from fromType: some Any, - to toType: ToType.Type, - file: String = #file, - function: String = #function, - line: Int = #line, - column: Int = #column - ) throws -> ToType { - guard let convertedValue = fromType as? ToType else { - throw NSError( - domain: "com.mapbox.copilot.developerError.failedTypeConversion", - code: -1, - userInfo: [ - "explanation": "Failed to convert \(String(describing: fromType)) to \(toType)", - "file": file, - "function": function, - "line": "\(line)", - "column": "\(column)", - ] - ) - } - return convertedValue - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryEvent.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryEvent.swift deleted file mode 100644 index 0b7fddf7e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryEvent.swift +++ /dev/null @@ -1,82 +0,0 @@ -import CoreLocation -import Foundation -import MapboxNavigationNative - -/// Describes history events produced by ``HistoryReader`` -public protocol HistoryEvent: Equatable, Sendable { - /// Point in time when this event occured. - var timestamp: TimeInterval { get } -} - -extension HistoryEvent { - func compare(to other: any HistoryEvent) -> Bool { - guard let other = other as? Self else { - return false - } - return self == other - } -} - -/// History event of when route was set. -public struct RouteAssignmentHistoryEvent: HistoryEvent { - public let timestamp: TimeInterval - /// ``NavigationRoutes`` that was set. - public let navigationRoutes: NavigationRoutes -} - -/// History event of when location was updated. -public struct LocationUpdateHistoryEvent: HistoryEvent { - /// Point in time when this event occured. - /// - /// This illustrates the moment when history event was recorded. This may differ from - /// ``LocationUpdateHistoryEvent/location``'s `timestamp` since it displays when particular location was reached. - public let timestamp: TimeInterval - /// `CLLocation` being set. - public let location: CLLocation -} - -/// History event of unrecognized type. -/// -/// Such events usually mean that this type of events is not yet supported or this one is for service use only. -public class UnknownHistoryEvent: HistoryEvent, @unchecked Sendable { - public static func == (lhs: UnknownHistoryEvent, rhs: UnknownHistoryEvent) -> Bool { - return lhs.timestamp == rhs.timestamp - } - - public let timestamp: TimeInterval - - init(timestamp: TimeInterval) { - self.timestamp = timestamp - } -} - -final class StatusUpdateHistoryEvent: UnknownHistoryEvent, @unchecked Sendable { - let monotonicTimestamp: TimeInterval - let status: NavigationStatus - - init(timestamp: TimeInterval, monotonicTimestamp: TimeInterval, status: NavigationStatus) { - self.monotonicTimestamp = monotonicTimestamp - self.status = status - - super.init(timestamp: timestamp) - } -} - -/// History event being pushed by the user -/// -/// Such events are created by calling ``HistoryRecording/pushHistoryEvent(type:jsonData:)``. -public struct UserPushedHistoryEvent: HistoryEvent { - public let timestamp: TimeInterval - /// The event type specified for this custom event. - public let type: String - /// The data value that contains a valid JSON attached to the event. - /// - /// This value was provided by user with `HistoryRecording.pushHistoryEvent` method's `dictionary` argument. - public let properties: String - - init(timestamp: TimeInterval, type: String, properties: String) { - self.type = type - self.properties = properties - self.timestamp = timestamp - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReader.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReader.swift deleted file mode 100644 index a51b2a3a0..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReader.swift +++ /dev/null @@ -1,198 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// Digest of history file contents produced by ``HistoryReader``. -public struct History { - /// Array of recorded events in chronological order. - public fileprivate(set) var events: [any HistoryEvent] = [] - - /// Initial ``NavigationRoutes`` that was set to the Navigator. - /// - /// Can be `nil` if current file is a free drive recording or if history recording was started after such event. In - /// latter case this property may contain another ``NavigationRoutes`` which was, for example, set as a result of a - /// reroute event. - public var initialRoute: NavigationRoutes? { - return (events.first { event in - return event is RouteAssignmentHistoryEvent - } as? RouteAssignmentHistoryEvent)?.navigationRoutes - } - - /// Array of location updates. - public var rawLocations: [CLLocation] { - return events.compactMap { - return ($0 as? LocationUpdateHistoryEvent)?.location - } - } - - func rawLocationsShiftedToPresent() -> [CLLocation] { - return rawLocations.enumerated().map { CLLocation( - coordinate: $0.element.coordinate, - altitude: $0.element.altitude, - horizontalAccuracy: $0.element.horizontalAccuracy, - verticalAccuracy: $0.element.verticalAccuracy, - course: $0.element.course, - speed: $0.element.speed, - timestamp: Date() + TimeInterval($0.offset) - ) } - } -} - -/// Provides event-by-event access to history files contents. -/// -/// Supports `pbf.gz` files. History files are created by ``HistoryRecording/stopRecordingHistory(writingFileWith:)`` -/// and saved to ``HistoryRecordingConfig/historyDirectoryURL``. -public struct HistoryReader: AsyncSequence, Sendable { - public typealias Element = HistoryEvent - - /// Configures ``HistoryReader`` parsing options. - public struct ReadOptions: OptionSet, Sendable { - public var rawValue: UInt - public init(rawValue: UInt) { - self.rawValue = rawValue - } - - /// Reader will skip ``UnknownHistoryEvent`` events. - public static let omitUnknownEvents = ReadOptions(rawValue: 1) - } - - public struct AsyncIterator: AsyncIteratorProtocol { - private let historyReader: MapboxNavigationNative.HistoryReader - private let readOptions: ReadOptions? - - init(historyReader: MapboxNavigationNative.HistoryReader, readOptions: ReadOptions? = nil) { - self.historyReader = historyReader - self.readOptions = readOptions - } - - public mutating func next() async -> (any HistoryEvent)? { - guard let record = historyReader.next() else { - return nil - } - let event = await process(record: record) - if readOptions?.contains(.omitUnknownEvents) ?? false, event is UnknownHistoryEvent { - return await next() - } - return event - } - - private func process(record: HistoryRecord) async -> (any HistoryEvent)? { - let timestamp = TimeInterval(Double(record.timestampNanoseconds) / 1e9) - switch record.type { - case .setRoute: - guard let event = record.setRoute, - let navigationRoutes = await process(setRoute: event) else { break } - return RouteAssignmentHistoryEvent( - timestamp: timestamp, - navigationRoutes: navigationRoutes - ) - case .updateLocation: - guard let event = record.updateLocation else { break } - return LocationUpdateHistoryEvent( - timestamp: timestamp, - location: process(updateLocation: event) - ) - case .getStatus: - guard let event = record.getStatus else { break } - return StatusUpdateHistoryEvent( - timestamp: timestamp, - monotonicTimestamp: TimeInterval(Double( - event - .monotonicTimestampNanoseconds - ) / 1e9), - status: event.result - ) - case .pushHistory: - guard let event = record.pushHistory else { break } - return UserPushedHistoryEvent( - timestamp: timestamp, - type: event.type, - properties: event.properties - ) - @unknown default: - break - } - return UnknownHistoryEvent(timestamp: timestamp) - } - - private func process(setRoute: SetRouteHistoryRecord) async -> NavigationRoutes? { - guard let routeRequest = setRoute.routeRequest, - let routeResponse = setRoute.routeResponse, - routeRequest != "{}", routeResponse != "{}", - let responseData = routeResponse.data(using: .utf8) - else { - // Route reset - return nil - } - let routeIndex = Int(setRoute.routeIndex) - let routes = RouteParser.parseDirectionsResponse( - forResponseDataRef: .init(data: responseData), - request: routeRequest, - routeOrigin: setRoute.origin - ) - - guard routes.isValue(), - var nativeRoutes = routes.value as? [RouteInterface], - nativeRoutes.indices.contains(routeIndex) - else { - assertionFailure("Failed to parse set route event") - return nil - } - let routesData = RouteParser.createRoutesData( - forPrimaryRoute: nativeRoutes.remove(at: routeIndex), - alternativeRoutes: nativeRoutes - ) - return try? await NavigationRoutes(routesData: routesData) - } - - private func process(updateLocation: UpdateLocationHistoryRecord) -> CLLocation { - return CLLocation(updateLocation.location) - } - } - - public func makeAsyncIterator() -> AsyncIterator { - return AsyncIterator( - historyReader: MapboxNavigationNative.HistoryReader(path: fileUrl.path), - readOptions: readOptions - ) - } - - private let fileUrl: URL - private let readOptions: ReadOptions? - - /// Creates a new ``HistoryReader`` - /// - /// - parameter fileUrl: History file to read through. - public init?(fileUrl: URL, readOptions: ReadOptions? = nil) { - guard FileManager.default.fileExists(atPath: fileUrl.path) else { - return nil - } - self.fileUrl = fileUrl - self.readOptions = readOptions - } - - /// Creates a new ``HistoryReader`` instance. - /// - /// - parameter data: History data to read through. - public init?(data: Data, readOptions: ReadOptions? = nil) { - let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - do { - try data.write(to: temporaryURL, options: .withoutOverwriting) - } catch { - return nil - } - self.fileUrl = temporaryURL - self.readOptions = readOptions - } - - /// Performs synchronous full file read. - /// - /// This will read current file from beginning to the end. - /// - returns: ``History`` containing extracted events. - public func parse() async throws -> History { - var result = History() - for await event in self { - result.events.append(event) - } - return result - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecorder.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecorder.swift deleted file mode 100644 index 391eefdc7..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecorder.swift +++ /dev/null @@ -1,46 +0,0 @@ -import MapboxNavigationNative - -struct HistoryRecorder: HistoryRecording, @unchecked Sendable { - private let handle: HistoryRecorderHandle - - init(handle: HistoryRecorderHandle) { - self.handle = handle - } - - func startRecordingHistory() { - Task { @MainActor in - handle.startRecording() - } - } - - func pushHistoryEvent(type: String, jsonData: Data?) { - let jsonString: String - if let jsonData { - guard let value = String(data: jsonData, encoding: .utf8) else { - assertionFailure("Failed to decode string") - return - } - jsonString = value - } else { - jsonString = "" - } - Task { @MainActor in - handle.pushHistory( - forEventType: type, - eventJson: jsonString - ) - } - } - - func stopRecordingHistory(writingFileWith completionHandler: @escaping HistoryFileWritingCompletionHandler) { - Task { @MainActor in - handle.stopRecording { path in - if let path { - completionHandler(URL(fileURLWithPath: path)) - } else { - completionHandler(nil) - } - } - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecording.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecording.swift deleted file mode 100644 index 90283b001..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryRecording.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation - -/// Types that conform to this protocol record low-level details as the user goes through a trip for debugging purposes. -public protocol HistoryRecording: Sendable { - /// A closure to be called when history writing ends. - /// - Parameters: - /// - historyFileURL: A URL to the file that contains history data. This argument is `nil` if no history data has - /// been written because history recording has not yet begun. Use the - /// ``HistoryRecording/startRecordingHistory()`` method to begin recording before attempting to write a history - /// file. - typealias HistoryFileWritingCompletionHandler = @Sendable (_ historyFileURL: URL?) -> Void - - /// Starts recording history for debugging purposes. - /// - /// - Postcondition: Use the ``HistoryRecording/stopRecordingHistory(writingFileWith:)`` method to stop recording - /// history and write the recorded history to a file. - func startRecordingHistory() - - /// Appends a custom event to the current history log. This can be useful to log things that happen during - /// navigation that are specific to your application. - /// - Parameters: - /// - type: The event type in the events log for your custom event. - /// - jsonData: The data value that contains a valid JSON to attach to the event. - func pushHistoryEvent(type: String, jsonData: Data?) - - /// Stops recording history, asynchronously writing any recorded history to a file. - /// - /// Upon completion, the completion handler is called with the URL to a file in the directory specified by - /// ``HistoryRecordingConfig/historyDirectoryURL``. The file contains details about the passive location manager’s - /// activity that may be useful to include when reporting an issue to Mapbox. - /// - Precondition: Use the ``HistoryRecording/startRecordingHistory()`` method to begin recording history. If the - /// ``HistoryRecording/startRecordingHistory()`` method has not been called, this method has no effect. - /// - Postcondition: To write history incrementally without an interruption in history recording, use the - /// ``HistoryRecording/startRecordingHistory()`` method immediately after this method. If you use the - /// ``HistoryRecording/startRecordingHistory()`` method inside the completion handler of this method, history - /// recording will be paused while the file is being prepared. - /// - Parameter completionHandler: A closure to be executed when the history file is ready. - func stopRecordingHistory(writingFileWith completionHandler: @escaping HistoryFileWritingCompletionHandler) -} - -/// Convenience methods for ``HistoryRecording`` protocol. -extension HistoryRecording { - /// Appends a custom event to the current history log. This can be useful to log things that happen during - /// navigation that are specific to your application. - /// - Precondition: Use the ``HistoryRecording/startRecordingHistory()`` method to begin recording history. If the - /// ``HistoryRecording/startRecordingHistory()`` method has not been called, this method has no effect. - /// - Parameters: - /// - type: The event type in the events log for your custom event. - /// - value: The value that implements `Encodable` protocol and can be encoded into a valid JSON to attach to the - /// event. - /// - encoder: The instance of `JSONEncoder` to be used for the value encoding. If this argument is omitted, the - /// default `JSONEncoder` will be used. - public func pushHistoryEvent(type: String, value: (some Encodable)?, encoder: JSONEncoder? = nil) throws { - let data = try value.map { value -> Data in - try (encoder ?? JSONEncoder()).encode(value) - } - pushHistoryEvent(type: type, jsonData: data) - } - - /// Appends a custom event to the current history log. This can be useful to log things that happen during - /// navigation that are specific to your application. - /// - Precondition: Use the ``HistoryRecording/startRecordingHistory()`` method to begin recording history. If the - /// ``HistoryRecording/startRecordingHistory()`` method has not been called, this method has no effect. - /// - Parameters: - /// - type: The event type in the events log for your custom event. - /// - value: The value disctionary that can be encoded into a JSON to attach to the event. - public func pushHistoryEvent(type: String, dictionary value: [String: Any?]?) throws { - let data = try value.map { value -> Data in - try JSONSerialization.data(withJSONObject: value, options: []) - } - pushHistoryEvent(type: type, jsonData: data) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReplayer.swift b/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReplayer.swift deleted file mode 100644 index 0c3f3c9e0..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/History/HistoryReplayer.swift +++ /dev/null @@ -1,351 +0,0 @@ -import _MapboxNavigationHelpers -import Combine -import CoreLocation -import Foundation - -/// Provides controls over the history replaying playback. -/// -/// This class is used together with ``LocationClient/historyReplayingValue(with:)`` to create and control history files -/// playback. Use this instance to observe playback events, seek, pause and controll playback speed. -public final class HistoryReplayController: Sendable { - private struct State: Sendable { - weak var delegate: HistoryReplayDelegate? - var currentEvent: (any HistoryEvent)? - var isPaused: Bool - var replayStart: TimeInterval? - var speedMultiplier: Double - } - - private let _state: UnfairLocked - - /// ``HistoryReplayDelegate`` instance for observing replay events. - public weak var delegate: HistoryReplayDelegate? { - get { _state.read().delegate } - set { _state.mutate { $0.delegate = newValue } } - } - - /// Playback speed. - /// - /// May be useful for reaching the required portion of the history trace. - /// - important: Too high values may result in navigator not being able to correctly process the events and thus - /// leading to the undefined behavior. Also, the value must be greater than 0. - public var speedMultiplier: Double { - get { _state.read().speedMultiplier } - set { - precondition(newValue > 0.0, "HistoryReplayController.speedMultiplier must be greater than 0!") - _state.mutate { $0.speedMultiplier = newValue } - } - } - - /// A stream of location updates contained in the history trace. - public var locations: AnyPublisher { - _locations.read().eraseToAnyPublisher() - } - - private let _locations: UnfairLocked> = .init(.init()) - private var currentEvent: (any HistoryEvent)? { - get { _state.read().currentEvent } - set { _state.mutate { $0.currentEvent = newValue } } - } - - private let eventsIteratorLocked: UnfairLocked - private func getNextEvent() async -> (any HistoryEvent)? { - var iterator = eventsIteratorLocked.read() - let result = try? await iterator.next() - eventsIteratorLocked.update(iterator) - return result - } - - /// Indicates if the playback was paused. - public var isPaused: Bool { - _state.read().isPaused - } - - private var replayStart: TimeInterval? { - get { _state.read().replayStart } - set { _state.mutate { $0.replayStart = newValue } } - } - - /// Creates new ``HistoryReplayController`` instance. - /// - parameter history: Parsed ``History`` instance, containing stream of events for playback. - public convenience init(history: History) { - self.init( - eventsIterator: .init( - datasource: .historyFile(history, 0) - ) - ) - } - - /// Creates new ``HistoryReplayController`` instance. - /// - parameter historyReader: ``HistoryReader`` instance, used to fetch and replay the history events. - public convenience init(historyReader: HistoryReader) { - self.init( - eventsIterator: .init( - datasource: .historyIterator( - historyReader.makeAsyncIterator() - ) - ) - ) - } - - fileprivate init(eventsIterator: EventsIterator) { - self._state = .init( - .init( - delegate: nil, - currentEvent: nil, - isPaused: true, - replayStart: nil, - speedMultiplier: 1 - ) - ) - self.eventsIteratorLocked = .init(eventsIterator) - } - - /// Seeks forward to the specific event. - /// - /// When found, this event will be the next one processed and reported via the ``HistoryReplayController/delegate``. - /// If such event was not found, controller will seek to the end of the trace. - /// It is not possible to seek backwards. - /// - parameter event: ``HistoryEvent`` to seek to. - /// - returns: `True` if seek was successfull, `False` - otherwise. - public func seekTo(event: any HistoryEvent) async -> Bool { - if replayStart == nil { - currentEvent = await getNextEvent() - replayStart = currentEvent?.timestamp - } - guard replayStart ?? .greatestFiniteMagnitude <= event.timestamp, - currentEvent?.timestamp ?? .greatestFiniteMagnitude <= event.timestamp - else { - return false - } - var nextEvent = if let currentEvent { - currentEvent - } else { - await getNextEvent() - } - - while let checkedEvent = nextEvent, - !checkedEvent.compare(to: event) - { - nextEvent = await getNextEvent() - } - currentEvent = nextEvent - guard nextEvent != nil else { - return false - } - return true - } - - /// Seeks forward to the specific time offset, relative to the beginning of the replay. - /// - /// The next event reported via the ``HistoryReplayController/delegate`` will have it's offset relative to the - /// beginning of the replay be not less then `offset` parameter. - /// It is not possible to seek backwards. - /// - parameter offset: Seek to this offset, relative to the beginning of the replay. If `offset` is greater then - /// replay total duration - controller will seek to the end of the trace. - /// - returns: `True` if seek was successfull, `False` - otherwise. - public func seekTo(offset: TimeInterval) async -> Bool { - if let currentEvent, - let currentOffset = eventOffest(currentEvent), - currentOffset > offset - { - return false - } - - var nextEvent = if let currentEvent { - currentEvent - } else { - await getNextEvent() - } - replayStart = replayStart ?? nextEvent?.timestamp - - while let checkedEvent = nextEvent, - eventOffest(checkedEvent) ?? .greatestFiniteMagnitude < offset - { - nextEvent = await getNextEvent() - } - currentEvent = nextEvent - guard nextEvent != nil else { - return false - } - return true - } - - /// Starts of resumes the playback. - public func play() { - guard isPaused else { return } - _state.mutate { - $0.isPaused = false - } - processEvent(currentEvent) - } - - /// Pauses the playback. - public func pause() { - _state.mutate { - $0.isPaused = true - } - } - - /// Manually pushes the location, as if it was in the replay. - /// - /// May be useful to setup the replay by providing initial location to begin with. - /// - parameter location: `CLLocation` to be pushed through the replay. - public func push(location: CLLocation) { - _locations.read().send(location) - } - - /// Replaces history events in the playback queue. - /// - parameter history: Parsed ``History`` instance, containing stream of events for playback. - public func push(events history: History) { - eventsIteratorLocked.update( - .init( - datasource: .historyFile(history, 0) - ) - ) - } - - /// Replaces history events in the playback queue. - /// - parameter historyReader: ``HistoryReader`` instance, used to fetch and replay the history events. - public func push(events historyReader: HistoryReader) { - eventsIteratorLocked.update( - .init( - datasource: .historyIterator( - historyReader.makeAsyncIterator() - ) - ) - ) - } - - /// Clears the playback queue. - public func clearEvents() { - eventsIteratorLocked.update(.init(datasource: nil)) - currentEvent = nil - replayStart = nil - } - - /// Calculates event's time offset, relative to the beginning of the replay. - /// - /// It does not check if passed event is actually in the replay. The replay must be started (at least 1 event should - /// be processed), before this method could calculate offsets. - /// - parameter event: An event to calculate it's relative time offset. - /// - returns: Event's time offset, relative to the beginning of the replay, or `nil` if current replay was not - /// started yet. - public func eventOffest(_ event: any HistoryEvent) -> TimeInterval? { - replayStart.map { event.timestamp - $0 } - } - - func tick() async { - var eventDelay = currentEvent?.timestamp - currentEvent = await getNextEvent() - guard let currentEvent else { - Task { @MainActor in - delegate?.historyReplayControllerDidFinishReplay(self) - } - return - } - if replayStart == nil { - replayStart = currentEvent.timestamp - } - - eventDelay = currentEvent.timestamp - (eventDelay ?? currentEvent.timestamp) - DispatchQueue.main.asyncAfter(deadline: .now() + (eventDelay ?? 0.0) / speedMultiplier) { [weak self] in - guard let self else { return } - processEvent(currentEvent) - } - } - - private func processEvent(_ event: (any HistoryEvent)?) { - guard !isPaused else { - return - } - defer { - Task.detached { [self] in - await self.tick() - } - } - guard let event else { return } - switch event { - case let locationEvent as LocationUpdateHistoryEvent: - _locations.read().send(locationEvent.location) - case let setRouteEvent as RouteAssignmentHistoryEvent: - delegate?.historyReplayController( - self, - wantsToSetRoutes: setRouteEvent.navigationRoutes - ) - default: - break - } - delegate?.historyReplayController(self, didReplayEvent: event) - } -} - -/// Delegate for ``HistoryReplayController``. -/// -/// Has corresponding methods to observe when particular event has ocurred or when the playback is finished. -public protocol HistoryReplayDelegate: AnyObject, Sendable { - /// Called after each ``HistoryEvent`` was handled by the ``HistoryReplayController``. - /// - parameter controller: A ``HistoryReplayController`` which has handled the event. - /// - parameter event: ``HistoryEvent`` that was just replayed. - func historyReplayController(_ controller: HistoryReplayController, didReplayEvent event: any HistoryEvent) - /// Called when ``HistoryReplayController`` has reached a ``RouteAssignmentHistoryEvent`` and reports that new - /// ``NavigationRoutes`` should be set to the navigator. - /// - parameter controller: A ``HistoryReplayController`` which has handled the event. - /// - parameter navigationRoutes: ``NavigationRoutes`` to be set to the navigator. - func historyReplayController( - _ controller: HistoryReplayController, - wantsToSetRoutes navigationRoutes: NavigationRoutes - ) - /// Called when ``HistoryReplayController`` has reached the end of the replay and finished. - /// - parameter controller: The related ``HistoryReplayController``. - func historyReplayControllerDidFinishReplay(_ controller: HistoryReplayController) -} - -extension LocationClient { - /// Creates a simulation ``LocationClient`` which will replay locations and other events from the history file. - /// - parameter controller: ``HistoryReplayController`` instance used to control and observe the playback. - /// - returns: ``LocationClient``, configured for replaying the history trace. - public static func historyReplayingValue(with controller: HistoryReplayController) -> Self { - return Self( - locations: controller.locations, - headings: Empty().eraseToAnyPublisher(), - startUpdatingLocation: { controller.play() }, - stopUpdatingLocation: { controller.pause() }, - startUpdatingHeading: {}, - stopUpdatingHeading: {} - ) - } -} - -private struct EventsIterator: AsyncIteratorProtocol { - typealias Element = any HistoryEvent - enum Datasource { - case historyIterator(HistoryReader.AsyncIterator) - case historyFile(History, Int) - } - - var datasource: Datasource? - - mutating func next() async throws -> (any HistoryEvent)? { - switch datasource { - case .historyIterator(var asyncIterator): - defer { - self.datasource = .historyIterator(asyncIterator) - } - // This line may trigger bindgen `Function HistoryReader::next called from a thread that is not owning the - // object` error log, if the replayer was instantiated on the main thread. - // This is not an error by itself, but it indicates possible thread safety violation using this iterator. - return await asyncIterator.next() - case .historyFile(let history, let index): - defer { - self.datasource = .historyFile(history, index + 1) - } - guard history.events.indices ~= index else { - return nil - } - return history.events[index] - case .none: - return nil - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/IdleTimerManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/IdleTimerManager.swift deleted file mode 100644 index d97584ec1..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/IdleTimerManager.swift +++ /dev/null @@ -1,84 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import UIKit - -/// An idle timer which is managed by `IdleTimerManager`. -protocol IdleTimer: Sendable { - func setDisabled(_ disabled: Bool) -} - -/// UIApplication specific idle timer. -struct UIApplicationIdleTimer: IdleTimer { - func setDisabled(_ disabled: Bool) { - // Using @MainActor task is unsafe as order of Tasks is undefined. DispatchQueue.main is easiest solution. - DispatchQueue.main.async { - UIApplication.shared.isIdleTimerDisabled = disabled - } - } -} - -/// Manages `UIApplication.shared.isIdleTimerDisabled`. -public final class IdleTimerManager: Sendable { - private typealias ID = String - - /// While a cancellable isn't cancelled, the idle timer is disabled. A cancellable is cancelled on deallocation. - public final class Cancellable: Sendable { - private let onCancel: @Sendable () -> Void - - fileprivate init(_ onCancel: @escaping @Sendable () -> Void) { - self.onCancel = onCancel - } - - deinit { - onCancel() - } - } - - public static let shared: IdleTimerManager = .init(idleTimer: UIApplicationIdleTimer()) - - private let idleTimer: IdleTimer - - /// Number of currently non cancelled `IdleTimerManager.Cancellable` instances. - private let cancellablesCount: UnfairLocked = .init(0) - - private let idleTokens: UnfairLocked<[ID: Cancellable]> = .init([:]) - - init(idleTimer: IdleTimer) { - self.idleTimer = idleTimer - } - - /// Disables idle timer `UIApplication.shared.isIdleTimerDisabled` while there is at least one non-cancelled - /// `IdleTimerManager.Cancellable` instance. - /// - Returns: An instance of cancellable that you should retain until you want the idle timer to be disabled. - public func disableIdleTimer() -> IdleTimerManager.Cancellable { - let cancellable = Cancellable { - self.changeCancellabelsCount(delta: -1) - } - changeCancellabelsCount(delta: 1) - return cancellable - } - - /// Disables the idle timer with the specified id. - /// - Parameter id: The id of the timer to disable. - public func disableIdleTimer(id: String) { - idleTokens.mutate { - $0[id] = disableIdleTimer() - } - } - - /// Enables the idle timer with the specified id. - /// - Parameter id: The id of the timer to enable. - public func enableIdleTimer(id: String) { - idleTokens.mutate { - $0[id] = nil - } - } - - private func changeCancellabelsCount(delta: Int) { - let isIdleTimerDisabled = cancellablesCount.mutate { - $0 += delta - return $0 > 0 - } - idleTimer.setDisabled(isIdleTimerDisabled) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Localization/LocalizationManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Localization/LocalizationManager.swift deleted file mode 100644 index 4d59f9781..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Localization/LocalizationManager.swift +++ /dev/null @@ -1,48 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation - -// swiftformat:disable enumNamespaces -/// This class handles the localization of the string inside of the SDK. -public struct LocalizationManager { - /// Set this bundle if you want to provide a custom localization for some string in the SDK. If the provided bundle - /// does not contain the localized version, the string from the default bundle inside the SDK will be used. - public static var customLocalizationBundle: Bundle? { - get { _customLocalizationBundle.read() } - set { _customLocalizationBundle.update(newValue) } - } - - private static let _customLocalizationBundle: NSLocked = .init(nil) - private static let nonExistentKeyValue = "_nonexistent_key_value_".uppercased() - - /// Retrieves the localized string for a given key. - /// - Parameters: - /// - key: The key for the string to localize. - /// - tableName: The name of the table containing the localized string identified by `key`. - /// - defaultBundle: The default bundle containing the table's strings file. - /// - value: The value to use if the key is not found (optional). - /// - comment: A note to the translator describing the context where the localized string is presented to the - /// user. - /// - Returns: A localized string. - public static func localizedString( - _ key: String, - tableName: String? = nil, - defaultBundle: Bundle, - value: String, - comment: String = "" - ) -> String { - if let customBundle = customLocalizationBundle { - let customString = NSLocalizedString( - key, - tableName: tableName, - bundle: customBundle, - value: nonExistentKeyValue, - comment: comment - ) - if customString != nonExistentKeyValue { - return customString - } - } - - return NSLocalizedString(key, tableName: tableName, bundle: defaultBundle, value: value, comment: comment) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Localization/String+Localization.swift b/ios/Classes/Navigation/MapboxNavigationCore/Localization/String+Localization.swift deleted file mode 100644 index 36fe246b6..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Localization/String+Localization.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation - -extension String { - func localizedString( - value: String, - tableName: String? = nil, - defaultBundle: Bundle = .mapboxNavigationUXCore, - comment: String = "" - ) -> String { - LocalizationManager.localizedString( - self, - tableName: tableName, - defaultBundle: defaultBundle, - value: value, - comment: comment - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/CameraStateTransition.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/CameraStateTransition.swift deleted file mode 100644 index 5ff60b9e8..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/CameraStateTransition.swift +++ /dev/null @@ -1,34 +0,0 @@ -import MapboxMaps -import UIKit - -/// Protocol, which is used to execute camera-related transitions, based on data provided via `CameraOptions` in -/// ``ViewportDataSource``. -@MainActor -public protocol CameraStateTransition: AnyObject { - // MARK: Updating the Camera - - /// A map view to which corresponding camera is related. - var mapView: MapView? { get } - - /// Initializer of ``CameraStateTransition`` object. - /// - /// - parameter mapView: `MapView` to which corresponding camera is related. - init(_ mapView: MapView) - - /// Performs a camera transition to new camera options. - /// - /// - parameter cameraOptions: An instance of `CameraOptions`, which describes a viewpoint of the `MapView`. - /// - parameter completion: A completion handler, which is called after performing the transition. - func transitionTo(_ cameraOptions: CameraOptions, completion: @escaping (() -> Void)) - - /// Performs a camera update, when already in the ``NavigationCameraState/overview`` state or - /// ``NavigationCameraState/following`` state. - /// - /// - parameter cameraOptions: An instance of `CameraOptions`, which describes a viewpoint of the `MapView`. - /// - parameter state: An instance of ``NavigationCameraState``, which describes the current state of - /// ``NavigationCamera``. - func update(to cameraOptions: CameraOptions, state: NavigationCameraState) - - /// Cancels the current transition. - func cancelPendingTransition() -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/FollowingCameraOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/FollowingCameraOptions.swift deleted file mode 100644 index d73071fe2..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/FollowingCameraOptions.swift +++ /dev/null @@ -1,260 +0,0 @@ -import CoreLocation - -/// Options, which are used to control what `CameraOptions` parameters will be modified by -/// ``ViewportDataSource`` in ``NavigationCameraState/following`` state. -public struct FollowingCameraOptions: Equatable, Sendable { - // MARK: Restricting the Orientation - - /// Pitch, which will be taken into account when preparing `CameraOptions` during active guidance - /// navigation. - /// - /// Defaults to `45.0` degrees. - /// - /// - Invariant: Acceptable range of values is `0...85`. - public var defaultPitch: Double = 45.0 { - didSet { - if defaultPitch < 0.0 { - defaultPitch = 0 - assertionFailure("Lower bound of the pitch should not be lower than 0.0") - } - - if defaultPitch > 85.0 { - defaultPitch = 85 - assertionFailure("Upper bound of the pitch should not be higher than 85.0") - } - } - } - - /// Zoom levels range, which will be used when producing camera frame in ``NavigationCameraState/following`` - /// state. - /// - /// Upper bound of the range will be also used as initial zoom level when active guidance navigation starts. - /// - /// Lower bound defaults to `10.50`, upper bound defaults to `16.35`. - /// - /// - Invariant: Acceptable range of values is `0...22`. - public var zoomRange: ClosedRange = 10.50...16.35 { - didSet { - let newValue = zoomRange - - if newValue.lowerBound < 0.0 || newValue.upperBound > 22.0 { - zoomRange = max(0, zoomRange.lowerBound)...min(22, zoomRange.upperBound) - } - - if newValue.lowerBound < 0.0 { - assertionFailure("Lower bound of the zoom range should not be lower than 0.0") - } - - if newValue.upperBound > 22.0 { - assertionFailure("Upper bound of the zoom range should not be higher than 22.0") - } - } - } - - // MARK: Camera Frame Modification Flags - - /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.center` property - /// when producing camera frame in ``NavigationCameraState/following`` state. - /// - /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.center` property. - /// - /// Defaults to `true`. - public var centerUpdatesAllowed = true - - /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.zoom` property - /// when producing camera frame in ``NavigationCameraState/following`` state. - /// - /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.zoom` property. - /// - /// Defaults to `true`. - public var zoomUpdatesAllowed: Bool = true - - /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.bearing` property - /// when producing camera frame in ``NavigationCameraState/following`` state. - /// - /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.bearing` property. - /// - /// Defaults to `true`. - public var bearingUpdatesAllowed: Bool = true - - /// - /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.pitch` property - /// when producing camera frame in ``NavigationCameraState/following`` state. - /// - /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.pitch` property. - /// - /// Defaults to `true`. - public var pitchUpdatesAllowed: Bool = true - - /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.padding` property - /// when producing camera frame in ``NavigationCameraState/following`` state. - /// - /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.padding` property. - /// - /// Defaults to `true`. - public var paddingUpdatesAllowed: Bool = true - - // MARK: Emphasizing the Upcoming Maneuver - - /// Options, which allow to modify the framed route geometries based on the intersection density. - /// - /// By default the whole remainder of the step is framed, while ``IntersectionDensity`` options shrink - /// that geometry to increase the zoom level. - public var intersectionDensity: IntersectionDensity = .init() - - /// Options, which allow to modify `CameraOptions.bearing` property based on information about - /// bearing of an upcoming maneuver. - public var bearingSmoothing: BearingSmoothing = .init() - - /// Options, which allow to modify framed route geometries by appending additional coordinates after - /// maneuver to extend the view. - public var geometryFramingAfterManeuver: GeometryFramingAfterManeuver = .init() - - /// Options, which allow to modify the framed route geometries when approaching a maneuver. - public var pitchNearManeuver: PitchNearManeuver = .init() - - /// If `true`, ``ViewportDataSource`` will follow course of the location. - /// - /// If `false`, ``ViewportDataSource`` will not follow course of the location and use `0.0` value instead. - public var followsLocationCourse = true - - /// Initializes ``FollowingCameraOptions`` instance. - public init() { - // No-op - } - - public static func == (lhs: FollowingCameraOptions, rhs: FollowingCameraOptions) -> Bool { - return lhs.defaultPitch == rhs.defaultPitch && - lhs.zoomRange == rhs.zoomRange && - lhs.centerUpdatesAllowed == rhs.centerUpdatesAllowed && - lhs.zoomUpdatesAllowed == rhs.zoomUpdatesAllowed && - lhs.bearingUpdatesAllowed == rhs.bearingUpdatesAllowed && - lhs.pitchUpdatesAllowed == rhs.pitchUpdatesAllowed && - lhs.paddingUpdatesAllowed == rhs.paddingUpdatesAllowed && - lhs.intersectionDensity == rhs.intersectionDensity && - lhs.bearingSmoothing == rhs.bearingSmoothing && - lhs.geometryFramingAfterManeuver == rhs.geometryFramingAfterManeuver && - lhs.pitchNearManeuver == rhs.pitchNearManeuver && - lhs.followsLocationCourse == rhs.followsLocationCourse - } -} - -/// Options, which allow to modify the framed route geometries based on the intersection density. -/// -/// By default the whole remainder of the step is framed, while `IntersectionDensity` options shrink -/// that geometry to increase the zoom level. -public struct IntersectionDensity: Equatable, Sendable { - /// Controls whether additional coordinates after the upcoming maneuver will be framed - /// to provide the view extension. - /// - /// Defaults to `true`. - public var enabled: Bool = true - - /// Multiplier, which will be used to adjust the size of the portion of the remaining step that's - /// going to be selected for framing. - /// - /// Defaults to `7.0`. - public var averageDistanceMultiplier: Double = 7.0 - - /// Minimum distance between intersections to count them as two instances. - /// - /// This has an effect of filtering out intersections based on parking lot entrances, - /// driveways and alleys from the average intersection distance. - /// - /// Defaults to `20.0` meters. - public var minimumDistanceBetweenIntersections: CLLocationDistance = 20.0 - - /// Initializes `IntersectionDensity` instance. - public init() { - // No-op - } - - public static func == (lhs: IntersectionDensity, rhs: IntersectionDensity) -> Bool { - return lhs.enabled == rhs.enabled && - lhs.averageDistanceMultiplier == rhs.averageDistanceMultiplier && - lhs.minimumDistanceBetweenIntersections == rhs.minimumDistanceBetweenIntersections - } -} - -/// Options, which allow to modify `CameraOptions.bearing` property based on information about -/// bearing of an upcoming maneuver. -public struct BearingSmoothing: Equatable, Sendable { - /// Controls whether bearing smoothing will be performed or not. - /// - /// Defaults to `true`. - public var enabled: Bool = true - - /// Controls how much the bearing can deviate from the location's bearing, in degrees. - /// - /// In case if set, the `bearing` property of `CameraOptions` during active guidance navigation - /// won't exactly reflect the bearing returned by the location, but will also be affected by the - /// direction to the upcoming framed geometry, to maximize the viewable area. - /// - /// Defaults to `45.0` degrees. - public var maximumBearingSmoothingAngle: CLLocationDirection = 45.0 - - /// Initializes ``BearingSmoothing`` instance. - public init() { - // No-op - } - - public static func == (lhs: BearingSmoothing, rhs: BearingSmoothing) -> Bool { - return lhs.enabled == rhs.enabled && - lhs.maximumBearingSmoothingAngle == rhs.maximumBearingSmoothingAngle - } -} - -/// Options, which allow to modify framed route geometries by appending additional coordinates after -/// maneuver to extend the view. -public struct GeometryFramingAfterManeuver: Equatable, Sendable { - /// Controls whether additional coordinates after the upcoming maneuver will be framed - /// to provide the view extension. - /// - /// Defaults to `true`. - public var enabled: Bool = true - - /// Controls the distance between maneuvers closely following the current one to include them - /// in the frame. - /// - /// Defaults to `150.0` meters. - public var distanceToCoalesceCompoundManeuvers: CLLocationDistance = 150.0 - - /// Controls the distance on the route after the current maneuver to include it in the frame. - /// - /// Defaults to `100.0` meters. - public var distanceToFrameAfterManeuver: CLLocationDistance = 100.0 - - /// Initializes ``GeometryFramingAfterManeuver`` instance. - public init() { - // No-op - } - - public static func == (lhs: GeometryFramingAfterManeuver, rhs: GeometryFramingAfterManeuver) -> Bool { - return lhs.enabled == rhs.enabled && - lhs.distanceToCoalesceCompoundManeuvers == rhs.distanceToCoalesceCompoundManeuvers && - lhs.distanceToFrameAfterManeuver == rhs.distanceToFrameAfterManeuver - } -} - -/// Options, which allow to modify the framed route geometries when approaching a maneuver. -public struct PitchNearManeuver: Equatable, Sendable { - /// Controls whether `CameraOptions.pitch` will be set to `0.0` near upcoming maneuver. - /// - /// Defaults to `true`. - public var enabled: Bool = true - - /// Threshold distance to the upcoming maneuver. - /// - /// Defaults to `180.0` meters. - public var triggerDistanceToManeuver: CLLocationDistance = 180.0 - - /// Initializes ``PitchNearManeuver`` instance. - public init() { - // No-op - } - - public static func == (lhs: PitchNearManeuver, rhs: PitchNearManeuver) -> Bool { - return lhs.enabled == rhs.enabled && - lhs.triggerDistanceToManeuver == rhs.triggerDistanceToManeuver - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCamera.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCamera.swift deleted file mode 100644 index 382cbbc9f..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCamera.swift +++ /dev/null @@ -1,243 +0,0 @@ -import _MapboxNavigationHelpers -import Combine -import CoreLocation -import Foundation -import MapboxDirections -import MapboxMaps -import UIKit - -/// ``NavigationCamera`` class provides functionality, which allows to manage camera-related states and transitions in a -/// typical navigation scenarios. It's fed with `CameraOptions` via the ``ViewportDataSource`` protocol and executes -/// transitions using ``CameraStateTransition`` protocol. - -@MainActor -public class NavigationCamera { - struct State: Equatable { - var cameraState: NavigationCameraState = .idle - var location: CLLocation? - var heading: CLHeading? - var routeProgress: RouteProgress? - var viewportPadding: UIEdgeInsets = .zero - } - - /// Notifies that the navigation camera state has changed. - public var cameraStates: AnyPublisher { - _cameraStates.eraseToAnyPublisher() - } - - private let _cameraStates: PassthroughSubject = .init() - - /// The padding applied to the viewport. - public var viewportPadding: UIEdgeInsets { - set { - state.viewportPadding = newValue - } - get { - state.viewportPadding - } - } - - private let _states: PassthroughSubject = .init() - - private var state: State = .init() { - didSet { - _states.send(state) - } - } - - private var lifetimeSubscriptions: Set = [] - private var isTransitioningCameraState: Bool = false - private var lastCameraState: NavigationCameraState = .idle - - /// Initializes ``NavigationCamera`` instance. - /// - Parameters: - /// - mapView: An instance of `MapView`, on which camera-related transitions will be executed. - /// - location: A publisher that emits current user location. - /// - routeProgress: A publisher that emits route navigation progress. - /// - heading: A publisher that emits current user heading. Defaults to `nil.` - /// - navigationCameraType: Type of ``NavigationCamera``, which is used for the current instance of - /// ``NavigationMapView``. - /// - viewportDataSource: An object is used to provide location-related data to perform camera-related updates - /// continuously. - /// - cameraStateTransition: An object, which is used to execute camera transitions. By default - /// ``NavigationCamera`` uses ``NavigationCameraStateTransition``. - public required init( - _ mapView: MapView, - location: AnyPublisher, - routeProgress: AnyPublisher, - heading: AnyPublisher? = nil, - navigationCameraType: NavigationCameraType = .mobile, - viewportDataSource: ViewportDataSource? = nil, - cameraStateTransition: CameraStateTransition? = nil - ) { - self.viewportDataSource = viewportDataSource ?? { - switch navigationCameraType { - case .mobile: - return MobileViewportDataSource(mapView) - case .carPlay: - return CarPlayViewportDataSource(mapView) - } - }() - self.cameraStateTransition = cameraStateTransition ?? NavigationCameraStateTransition(mapView) - observe(location: location) - observe(routeProgress: routeProgress) - observe(heading: heading) - observe(viewportDataSource: self.viewportDataSource) - - _states - .debounce(for: 0.2, scheduler: DispatchQueue.main) - .sink { [weak self] newState in - guard let self else { return } - if let location = newState.location { - self.viewportDataSource.update( - using: ViewportState( - location: location, - routeProgress: newState.routeProgress, - viewportPadding: viewportPadding, - heading: newState.heading - ) - ) - } - if newState.cameraState != lastCameraState { - update(using: newState.cameraState) - } - }.store(in: &lifetimeSubscriptions) - - // Uncomment to be able to see `NavigationCameraDebugView`. -// setupDebugView(mapView) - } - - /// Updates the current camera state. - /// - Parameter cameraState: A new camera state. - public func update(cameraState: NavigationCameraState) { - guard cameraState != state.cameraState else { return } - state.cameraState = cameraState - _cameraStates.send(cameraState) - } - - /// Call to this method immediately moves ``NavigationCamera`` to ``NavigationCameraState/idle`` state and stops all - /// pending transitions. - public func stop() { - update(cameraState: .idle) - cameraStateTransition.cancelPendingTransition() - } - - private var debugView: NavigationCameraDebugView? - - private func setupDebugView(_ mapView: MapView) { - let debugView = NavigationCameraDebugView(mapView, viewportDataSource: viewportDataSource) - self.debugView = debugView - mapView.addSubview(debugView) - } - - private func observe(location: AnyPublisher) { - location.sink { [weak self] in - self?.state.location = $0 - }.store(in: &lifetimeSubscriptions) - } - - private func observe(routeProgress: AnyPublisher) { - routeProgress.sink { [weak self] in - self?.state.routeProgress = $0 - }.store(in: &lifetimeSubscriptions) - } - - private func observe(heading: AnyPublisher?) { - guard let heading else { return } - heading.sink { [weak self] in - self?.state.heading = $0 - }.store(in: &lifetimeSubscriptions) - } - - private var viewportSubscription: [AnyCancellable] = [] - - private func observe(viewportDataSource: ViewportDataSource) { - viewportSubscription = [] - - viewportDataSource.navigationCameraOptions - .removeDuplicates() - .sink { [weak self] navigationCameraOptions in - guard let self else { return } - update(using: navigationCameraOptions) - }.store(in: &viewportSubscription) - - // To prevent the lengthy animation from the Null Island to the current location use the camera to transition to - // the following state. - // The following camera options zoom should be calculated before at the moment. - viewportDataSource.navigationCameraOptions - .filter { $0.followingCamera.zoom != nil } - .first() - .sink { [weak self] _ in - self?.update(cameraState: .following) - }.store(in: &viewportSubscription) - } - - private func update(using cameraState: NavigationCameraState) { - lastCameraState = cameraState - - switch cameraState { - case .idle: - break - case .following: - switchToViewportDatasourceCamera(isFollowing: true) - case .overview: - switchToViewportDatasourceCamera(isFollowing: false) - } - } - - private func cameraOptionsForCurrentState(from navigationCameraOptions: NavigationCameraOptions) -> CameraOptions? { - switch state.cameraState { - case .following: - return navigationCameraOptions.followingCamera - case .overview: - return navigationCameraOptions.overviewCamera - case .idle: - return nil - } - } - - private func update(using navigationCameraOptions: NavigationCameraOptions) { - guard !isTransitioningCameraState, - let options = cameraOptionsForCurrentState(from: navigationCameraOptions) else { return } - - cameraStateTransition.update(to: options, state: state.cameraState) - } - - // MARK: Changing NavigationCamera State - - /// A type, which is used to provide location related data to continuously perform camera-related updates. - /// By default ``NavigationMapView`` uses ``MobileViewportDataSource`` or ``CarPlayViewportDataSource`` depending on - /// the current ``NavigationCameraType``. - public var viewportDataSource: ViewportDataSource { - didSet { - observe(viewportDataSource: viewportDataSource) - } - } - - /// The current state of ``NavigationCamera``. Defaults to ``NavigationCameraState/idle``. - /// - /// Call ``update(cameraState:)`` to update this value. - public var currentCameraState: NavigationCameraState { - state.cameraState - } - - /// A type, which is used to execute camera transitions. - /// By default ``NavigationMapView`` uses ``NavigationCameraStateTransition``. - public var cameraStateTransition: CameraStateTransition - - private func switchToViewportDatasourceCamera(isFollowing: Bool) { - let cameraOptions: CameraOptions = { - if isFollowing { - return viewportDataSource.currentNavigationCameraOptions.followingCamera - } else { - return viewportDataSource.currentNavigationCameraOptions.overviewCamera - } - }() - isTransitioningCameraState = true - cameraStateTransition.transitionTo(cameraOptions) { [weak self] in - self?.isTransitioningCameraState = false - } - } -} - -extension CameraOptions: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraDebugView.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraDebugView.swift deleted file mode 100644 index cd1de7e61..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraDebugView.swift +++ /dev/null @@ -1,199 +0,0 @@ -import Combine -import MapboxMaps -import UIKit - -/// `UIView`, which is drawn on top of `MapView` and shows `CameraOptions` when ``NavigationCamera`` is in -/// ``NavigationCameraState/following`` state. -/// -/// Such `UIView` is useful for debugging purposes (especially when debugging camera behavior on CarPlay). -class NavigationCameraDebugView: UIView { - weak var mapView: MapView? - - weak var viewportDataSource: ViewportDataSource? { - didSet { - viewportDataSourceLifetimeSubscriptions.removeAll() - subscribe(to: viewportDataSource) - } - } - - private var viewportDataSourceLifetimeSubscriptions: Set = [] - - var viewportLayer = CALayer() - var viewportTextLayer = CATextLayer() - var anchorLayer = CALayer() - var anchorTextLayer = CATextLayer() - var centerLayer = CALayer() - var centerTextLayer = CATextLayer() - var pitchTextLayer = CATextLayer() - var zoomTextLayer = CATextLayer() - var bearingTextLayer = CATextLayer() - var centerCoordinateTextLayer = CATextLayer() - - required init( - _ mapView: MapView, - viewportDataSource: ViewportDataSource? - ) { - self.mapView = mapView - self.viewportDataSource = viewportDataSource - - super.init(frame: mapView.frame) - - isUserInteractionEnabled = false - backgroundColor = .clear - subscribe(to: viewportDataSource) - - viewportLayer.borderWidth = 3.0 - viewportLayer.borderColor = UIColor.green.cgColor - layer.addSublayer(viewportLayer) - - anchorLayer.backgroundColor = UIColor.red.cgColor - anchorLayer.frame = .init(x: 0.0, y: 0.0, width: 6.0, height: 6.0) - anchorLayer.cornerRadius = 3.0 - layer.addSublayer(anchorLayer) - - self.anchorTextLayer = CATextLayer() - anchorTextLayer.string = "Anchor" - anchorTextLayer.fontSize = UIFont.systemFontSize - anchorTextLayer.backgroundColor = UIColor.clear.cgColor - anchorTextLayer.foregroundColor = UIColor.red.cgColor - anchorTextLayer.frame = .zero - layer.addSublayer(anchorTextLayer) - - centerLayer.backgroundColor = UIColor.blue.cgColor - centerLayer.frame = .init(x: 0.0, y: 0.0, width: 6.0, height: 6.0) - centerLayer.cornerRadius = 3.0 - layer.addSublayer(centerLayer) - - self.centerTextLayer = CATextLayer() - centerTextLayer.string = "Center" - centerTextLayer.fontSize = UIFont.systemFontSize - centerTextLayer.backgroundColor = UIColor.clear.cgColor - centerTextLayer.foregroundColor = UIColor.blue.cgColor - centerTextLayer.frame = .zero - layer.addSublayer(centerTextLayer) - - self.pitchTextLayer = createDefaultTextLayer() - layer.addSublayer(pitchTextLayer) - - self.zoomTextLayer = createDefaultTextLayer() - layer.addSublayer(zoomTextLayer) - - self.bearingTextLayer = createDefaultTextLayer() - layer.addSublayer(bearingTextLayer) - - self.viewportTextLayer = createDefaultTextLayer() - layer.addSublayer(viewportTextLayer) - - self.centerCoordinateTextLayer = createDefaultTextLayer() - layer.addSublayer(centerCoordinateTextLayer) - } - - func createDefaultTextLayer() -> CATextLayer { - let textLayer = CATextLayer() - textLayer.string = "" - textLayer.fontSize = UIFont.systemFontSize - textLayer.backgroundColor = UIColor.clear.cgColor - textLayer.foregroundColor = UIColor.black.cgColor - textLayer.frame = .zero - - return textLayer - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func subscribe(to viewportDataSource: ViewportDataSource?) { - viewportDataSource?.navigationCameraOptions - .removeDuplicates() - .sink { [weak self] navigationCameraOptions in - guard let self else { return } - update(using: navigationCameraOptions) - }.store(in: &viewportDataSourceLifetimeSubscriptions) - } - - private func update(using navigationCameraOptions: NavigationCameraOptions) { - guard let mapView else { return } - - let camera = navigationCameraOptions.followingCamera - - if let anchorPosition = camera.anchor { - anchorLayer.position = anchorPosition - anchorTextLayer.frame = .init( - x: anchorLayer.frame.origin.x + 5.0, - y: anchorLayer.frame.origin.y + 5.0, - width: 80.0, - height: 20.0 - ) - } - - if let pitch = camera.pitch { - pitchTextLayer.frame = .init( - x: viewportLayer.frame.origin.x + 5.0, - y: viewportLayer.frame.origin.y + 5.0, - width: viewportLayer.frame.size.width - 10.0, - height: 20.0 - ) - pitchTextLayer.string = "Pitch: \(pitch)º" - } - - if let zoom = camera.zoom { - zoomTextLayer.frame = .init( - x: viewportLayer.frame.origin.x + 5.0, - y: viewportLayer.frame.origin.y + 30.0, - width: viewportLayer.frame.size.width - 10.0, - height: 20.0 - ) - zoomTextLayer.string = "Zoom: \(zoom)" - } - - if let bearing = camera.bearing { - bearingTextLayer.frame = .init( - x: viewportLayer.frame.origin.x + 5.0, - y: viewportLayer.frame.origin.y + 55.0, - width: viewportLayer.frame.size.width - 10.0, - height: 20.0 - ) - bearingTextLayer.string = "Bearing: \(bearing)º" - } - - if let edgeInsets = camera.padding { - viewportLayer.frame = CGRect( - x: edgeInsets.left, - y: edgeInsets.top, - width: mapView.frame.width - edgeInsets.left - edgeInsets.right, - height: mapView.frame.height - edgeInsets.top - edgeInsets.bottom - ) - - viewportTextLayer.frame = .init( - x: viewportLayer.frame.origin.x + 5.0, - y: viewportLayer.frame.origin.y + 80.0, - width: viewportLayer.frame.size.width - 10.0, - height: 20.0 - ) - viewportTextLayer - .string = - "Padding: (top: \(edgeInsets.top), left: \(edgeInsets.left), bottom: \(edgeInsets.bottom), right: \(edgeInsets.right))" - } - - if let centerCoordinate = camera.center { - centerLayer.position = mapView.mapboxMap.point(for: centerCoordinate) - centerTextLayer.frame = .init( - x: centerLayer.frame.origin.x + 5.0, - y: centerLayer.frame.origin.y + 5.0, - width: 80.0, - height: 20.0 - ) - - centerCoordinateTextLayer.frame = .init( - x: viewportLayer.frame.origin.x + 5.0, - y: viewportLayer.frame.origin.y + 105.0, - width: viewportLayer.frame.size.width - 10.0, - height: 20.0 - ) - centerCoordinateTextLayer - .string = "Center coordinate: (lat: \(centerCoordinate.latitude), lng:\(centerCoordinate.longitude))" - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraOptions.swift deleted file mode 100644 index 9d5172abb..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraOptions.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import MapboxMaps - -/// Represents calculated navigation camera options. -public struct NavigationCameraOptions: Equatable, Sendable { - /// `CameraOptions`, which are used when transitioning to ``NavigationCameraState/following`` or for continuous - /// updates when already in ``NavigationCameraState/following`` state. - public var followingCamera: CameraOptions - - /// `CameraOptions`, which are used when transitioning to ``NavigationCameraState/overview`` or for continuous - /// updates when already in ``NavigationCameraState/overview`` state. - public var overviewCamera: CameraOptions - - /// Creates a new ``NavigationCameraOptions`` instance. - /// - Parameters: - /// - followingCamera: `CameraOptions` used in the ``NavigationCameraState/following`` state. - /// - overviewCamera: `CameraOptions` used in the``NavigationCameraState/overview`` state. - public init(followingCamera: CameraOptions = .init(), overviewCamera: CameraOptions = .init()) { - self.followingCamera = followingCamera - self.overviewCamera = overviewCamera - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraState.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraState.swift deleted file mode 100644 index 4e1707782..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraState.swift +++ /dev/null @@ -1,24 +0,0 @@ -import MapboxMaps - -/// Defines camera behavior mode. -public enum NavigationCameraState: Equatable, Sendable { - /// The camera position and other attributes are idle. - case idle - /// The camera is following user position. - case following - /// The camera is previewing some extended, non-point object. - case overview -} - -extension NavigationCameraState: CustomDebugStringConvertible { - public var debugDescription: String { - switch self { - case .idle: - return "idle" - case .following: - return "following" - case .overview: - return "overview" - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraStateTransition.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraStateTransition.swift deleted file mode 100644 index 181cea65d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraStateTransition.swift +++ /dev/null @@ -1,209 +0,0 @@ -import MapboxMaps -import Turf -import UIKit - -/// The class, which conforms to ``CameraStateTransition`` protocol and provides default implementation of -/// camera-related transitions by using `CameraAnimator` functionality provided by Mapbox Maps SDK. -@MainActor -public class NavigationCameraStateTransition: CameraStateTransition { - // MARK: Transitioning State - - /// A map view to which corresponding camera is related. - public weak var mapView: MapView? - - var animatorCenter: BasicCameraAnimator? - var animatorZoom: BasicCameraAnimator? - var animatorBearing: BasicCameraAnimator? - var animatorPitch: BasicCameraAnimator? - var animatorAnchor: BasicCameraAnimator? - var animatorPadding: BasicCameraAnimator? - - var previousAnchor: CGPoint = .zero - - /// Initializer of ``NavigationCameraStateTransition`` object. - /// - Parameter mapView: `MapView` to which corresponding camera is related. - public required init(_ mapView: MapView) { - self.mapView = mapView - } - - /// Performs a camera transition to new camera options. - /// - /// - parameter cameraOptions: An instance of `CameraOptions`, which describes a viewpoint of the `MapView`. - /// - parameter completion: A completion handler, which is called after performing the transition. - public func transitionTo( - _ cameraOptions: CameraOptions, - completion: @escaping () -> Void - ) { - guard let mapView, - let zoom = cameraOptions.zoom - else { - completion() - return - } - - if let center = cameraOptions.center, !CLLocationCoordinate2DIsValid(center) { - completion() - return - } - - stopAnimators() - let duration: TimeInterval = mapView.mapboxMap.cameraState.zoom < zoom ? 0.5 : 0.25 - mapView.camera.fly(to: cameraOptions, duration: duration) { _ in - completion() - } - } - - /// Cancels the current transition. - public func cancelPendingTransition() { - stopAnimators() - } - - /// Performs a camera update, when already in the ``NavigationCameraState/overview`` state - /// or ``NavigationCameraState/following`` state. - /// - /// - parameter cameraOptions: An instance of `CameraOptions`, which describes a viewpoint of the `MapView`. - /// - parameter state: An instance of ``NavigationCameraState``, which describes the current state of - /// ``NavigationCamera``. - public func update(to cameraOptions: CameraOptions, state: NavigationCameraState) { - guard let mapView, - let center = cameraOptions.center, - CLLocationCoordinate2DIsValid(center), - let zoom = cameraOptions.zoom, - let bearing = (state == .overview) ? 0.0 : cameraOptions.bearing, - let pitch = cameraOptions.pitch, - let anchor = cameraOptions.anchor, - let padding = cameraOptions.padding else { return } - - let duration = 1.0 - let minimumCenterCoordinatePixelThreshold = 2.0 - let minimumPitchThreshold: CGFloat = 1.0 - let minimumBearingThreshold: CLLocationDirection = 1.0 - let timingParameters = UICubicTimingParameters( - controlPoint1: CGPoint(x: 0.0, y: 0.0), - controlPoint2: CGPoint(x: 1.0, y: 1.0) - ) - - // Check whether the location change is larger than a certain threshold when current camera state is following. - var updateCameraCenter = true - if state == .following { - let metersPerPixel = getMetersPerPixelAtLatitude(center.latitude, Double(zoom)) - let centerUpdateThreshold = minimumCenterCoordinatePixelThreshold * metersPerPixel - updateCameraCenter = (mapView.mapboxMap.cameraState.center.distance(to: center) > centerUpdateThreshold) - } - - if updateCameraCenter { - if let animatorCenter, animatorCenter.isRunning { - animatorCenter.stopAnimation() - } - - animatorCenter = mapView.camera.makeAnimator( - duration: duration, - timingParameters: timingParameters - ) { transition in - transition.center.toValue = center - } - - animatorCenter?.startAnimation() - } - - if let animatorZoom, animatorZoom.isRunning { - animatorZoom.stopAnimation() - } - - animatorZoom = mapView.camera.makeAnimator( - duration: duration, - timingParameters: timingParameters - ) { transition in - transition.zoom.toValue = zoom - } - - animatorZoom?.startAnimation() - - // Check whether the bearing change is larger than a certain threshold when current camera state is following. - let updateCameraBearing = (state == .following) ? - (abs(mapView.mapboxMap.cameraState.bearing - bearing) >= minimumBearingThreshold) : true - - if updateCameraBearing { - if let animatorBearing, animatorBearing.isRunning { - animatorBearing.stopAnimation() - } - - animatorBearing = mapView.camera.makeAnimator( - duration: duration, - timingParameters: timingParameters - ) { transition in - transition.bearing.toValue = bearing - } - - animatorBearing?.startAnimation() - } - - // Check whether the pitch change is larger than a certain threshold when current camera state is following. - let updateCameraPitch = (state == .following) ? - (abs(mapView.mapboxMap.cameraState.pitch - pitch) >= minimumPitchThreshold) : true - - if updateCameraPitch { - if let animatorPitch, animatorPitch.isRunning { - animatorPitch.stopAnimation() - } - - animatorPitch = mapView.camera.makeAnimator( - duration: duration, - timingParameters: timingParameters - ) { transition in - transition.pitch.toValue = pitch - } - - animatorPitch?.startAnimation() - } - - // In case if anchor did not change - do not perform animation. - let updateCameraAnchor = previousAnchor != anchor - previousAnchor = anchor - - if updateCameraAnchor { - if let animatorAnchor, animatorAnchor.isRunning { - animatorAnchor.stopAnimation() - } - - animatorAnchor = mapView.camera.makeAnimator( - duration: duration, - timingParameters: timingParameters - ) { transition in - transition.anchor.toValue = anchor - } - - animatorAnchor?.startAnimation() - } - - if let animatorPadding, animatorPadding.isRunning { - animatorPadding.stopAnimation() - } - - animatorPadding = mapView.camera.makeAnimator( - duration: duration, - timingParameters: timingParameters - ) { transition in - transition.padding.toValue = padding - } - - animatorPadding?.startAnimation() - } - - func stopAnimators() { - let animators = [ - animatorCenter, - animatorZoom, - animatorBearing, - animatorPitch, - animatorAnchor, - animatorPadding, - ] - mapView?.camera.cancelAnimations() - animators.compactMap { $0 }.forEach { - if $0.isRunning { - $0.stopAnimation() - } - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraType.swift deleted file mode 100644 index 72a8a940c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationCameraType.swift +++ /dev/null @@ -1,8 +0,0 @@ -/// Possible types of ``NavigationCamera``. -public enum NavigationCameraType { - /// When such type is used `MapboxMaps.CameraOptions` will be optimized specifically for CarPlay devices. - case carPlay - - /// Type, which is used for iPhone/iPad. - case mobile -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationViewportDataSourceOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationViewportDataSourceOptions.swift deleted file mode 100644 index cf24c2e47..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/NavigationViewportDataSourceOptions.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -/// Options, which give the ability to control whether certain `CameraOptions` will be generated -/// by ``ViewportDataSource`` or can be provided by user directly. -public struct NavigationViewportDataSourceOptions: Equatable, Sendable { - /// Options, which are used to control what `CameraOptions` parameters will be modified by - /// ``ViewportDataSource`` in ``NavigationCameraState/following`` state. - public var followingCameraOptions = FollowingCameraOptions() - - /// Options, which are used to control what `CameraOptions` parameters will be modified by - /// ``ViewportDataSource`` in ``NavigationCameraState/overview`` state. - public var overviewCameraOptions = OverviewCameraOptions() - - /// Initializes `NavigationViewportDataSourceOptions` instance. - public init() { - // No-op - } - - /// Initializes `NavigationViewportDataSourceOptions` instance. - /// - /// - parameter followingCameraOptions: `FollowingCameraOptions` instance, which contains - /// `CameraOptions` parameters, which in turn will be used by ``ViewportDataSource`` in - /// ``NavigationCameraState/following`` state. - /// - parameter overviewCameraOptions: `OverviewCameraOptions` instance, which contains - /// `CameraOptions` parameters, which it turn will be used by ``ViewportDataSource`` in - /// ``NavigationCameraState/overview`` state. - public init(followingCameraOptions: FollowingCameraOptions, overviewCameraOptions: OverviewCameraOptions) { - self.followingCameraOptions = followingCameraOptions - self.overviewCameraOptions = overviewCameraOptions - } - - public static func == (lhs: NavigationViewportDataSourceOptions, rhs: NavigationViewportDataSourceOptions) -> Bool { - return lhs.followingCameraOptions == rhs.followingCameraOptions && - lhs.overviewCameraOptions == rhs.overviewCameraOptions - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/OverviewCameraOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/OverviewCameraOptions.swift deleted file mode 100644 index 479b268fb..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/OverviewCameraOptions.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation - -/// Options, which are used to control what `CameraOptions` parameters will be modified by -/// ``ViewportDataSource`` in ``NavigationCameraState/overview`` state. -public struct OverviewCameraOptions: Equatable, Sendable { - /// Maximum zoom level, which will be used when producing camera frame in ``NavigationCameraState/overview`` - /// state. - /// - /// Defaults to `16.35`. - /// - /// - Invariant: Acceptable range of values is 0...22. - public var maximumZoomLevel: Double = 16.35 { - didSet { - if maximumZoomLevel < 0.0 { - maximumZoomLevel = 0 - assertionFailure("Maximum zoom level should not be lower than 0.0") - } - - if maximumZoomLevel > 22.0 { - maximumZoomLevel = 22 - assertionFailure("Maximum zoom level should not be higher than 22.0") - } - } - } - - /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.center` property - /// when producing camera frame in ``NavigationCameraState/overview`` state. - /// - /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.center` property. - /// - /// Defaults to `true`. - public var centerUpdatesAllowed: Bool = true - - /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.zoom` property - /// when producing camera frame in ``NavigationCameraState/overview`` state. - /// - /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.zoom` property. - /// - /// Defaults to `true`. - public var zoomUpdatesAllowed: Bool = true - - /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.bearing` property - /// when producing camera frame in ``NavigationCameraState/overview`` state. - /// - /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.bearing` property. - /// - /// Defaults to `true`. - public var bearingUpdatesAllowed: Bool = true - - /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.pitch` property - /// when producing camera frame in ``NavigationCameraState/overview`` state. - /// - /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.pitch` property. - /// - /// Defaults to `true`. - public var pitchUpdatesAllowed: Bool = true - - /// If `true`, ``ViewportDataSource`` will continuously modify `CameraOptions.padding` property - /// when producing camera frame in ``NavigationCameraState/overview`` state. - /// - /// If `false`, ``ViewportDataSource`` will not modify `CameraOptions.padding` property. - /// - /// Defaults to `true`. - public var paddingUpdatesAllowed: Bool = true - - /// Initializes ``OverviewCameraOptions`` instance. - public init() { - // No-op - } - - public static func == (lhs: OverviewCameraOptions, rhs: OverviewCameraOptions) -> Bool { - return lhs.maximumZoomLevel == rhs.maximumZoomLevel && - lhs.zoomUpdatesAllowed == rhs.zoomUpdatesAllowed && - lhs.bearingUpdatesAllowed == rhs.bearingUpdatesAllowed && - lhs.pitchUpdatesAllowed == rhs.pitchUpdatesAllowed && - lhs.paddingUpdatesAllowed == rhs.paddingUpdatesAllowed - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CarPlayViewportDataSource.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CarPlayViewportDataSource.swift deleted file mode 100644 index defe5abf7..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CarPlayViewportDataSource.swift +++ /dev/null @@ -1,312 +0,0 @@ -import Combine -import CoreLocation -import MapboxDirections -import MapboxMaps -import Turf -import UIKit - -/// The class, which conforms to ``ViewportDataSource`` protocol and provides default implementation of it for CarPlay. -@MainActor -public class CarPlayViewportDataSource: ViewportDataSource { - private var commonDataSource: CommonViewportDataSource - - weak var mapView: MapView? - - /// Initializes ``CarPlayViewportDataSource`` instance. - /// - Parameter mapView: An instance of `MapView`, which is going to be used for viewport calculation. `MapView` - /// will be weakly stored by ``CarPlayViewportDataSource``. - public required init(_ mapView: MapView) { - self.mapView = mapView - self.commonDataSource = .init(mapView) - } - - /// Options, which give the ability to control whether certain `CameraOptions` will be generated. - public var options: NavigationViewportDataSourceOptions { - get { commonDataSource.options } - set { commonDataSource.options = newValue } - } - - /// Notifies that the navigation camera options have changed in response to a viewport change. - public var navigationCameraOptions: AnyPublisher { - commonDataSource.navigationCameraOptions - } - - /// The last calculated ``NavigationCameraOptions``. - public var currentNavigationCameraOptions: NavigationCameraOptions { - get { - commonDataSource.currentNavigationCameraOptions - } - - set { - commonDataSource.currentNavigationCameraOptions = newValue - } - } - - /// Updates ``NavigationCameraOptions`` accoridng to the navigation state. - /// - Parameters: - /// - viewportState: The current viewport state. - public func update(using viewportState: ViewportState) { - commonDataSource.update(using: viewportState) { [weak self] state in - guard let self else { return nil } - return NavigationCameraOptions( - followingCamera: newFollowingCamera(with: state), - overviewCamera: newOverviewCamera(with: state) - ) - } - } - - private func newFollowingCamera(with state: ViewportDataSourceState) -> CameraOptions { - guard let mapView else { return .init() } - - let followingCameraOptions = options.followingCameraOptions - - var newOptions = currentNavigationCameraOptions.followingCamera - if let location = state.location, state.navigationState.isInPassiveNavigationOrCompletedActive { - if followingCameraOptions.centerUpdatesAllowed || followingCamera.center == nil { - newOptions.center = location.coordinate - } - - if followingCameraOptions.zoomUpdatesAllowed || followingCamera.zoom == nil { - let altitude = 1700.0 - let zoom = CGFloat(ZoomLevelForAltitude( - altitude, - mapView.mapboxMap.cameraState.pitch, - location.coordinate.latitude, - mapView.bounds.size - )) - - newOptions.zoom = zoom - } - - if followingCameraOptions.bearingUpdatesAllowed || followingCamera.bearing == nil { - if followingCameraOptions.followsLocationCourse { - newOptions.bearing = location.course - } else { - newOptions.bearing = 0.0 - } - } - - newOptions.anchor = mapView.center - - if followingCameraOptions.pitchUpdatesAllowed || followingCamera.pitch == nil { - newOptions.pitch = 0.0 - } - - if followingCameraOptions.paddingUpdatesAllowed || followingCamera.padding == nil { - newOptions.padding = mapView.safeAreaInsets - } - - return newOptions - } - - if let location = state.location, case .active(let activeState) = state.navigationState, - !activeState.isRouteComplete - { - let coordinatesToManeuver = activeState.coordinatesToManeuver - let lookaheadDistance = activeState.lookaheadDistance - - var compoundManeuvers: [[CLLocationCoordinate2D]] = [] - let geometryFramingAfterManeuver = followingCameraOptions.geometryFramingAfterManeuver - let pitchСoefficient = pitchСoefficient( - distanceRemainingOnStep: activeState.distanceRemainingOnStep, - currentCoordinate: location.coordinate, - currentLegStepIndex: activeState.currentLegStepIndex, - currentLegSteps: activeState.currentLegSteps - ) - let pitch = followingCameraOptions.defaultPitch * pitchСoefficient - var carPlayCameraPadding = mapView.safeAreaInsets + UIEdgeInsets.centerEdgeInsets - - // Bottom of the viewport on CarPlay should be placed at the same level with - // trip estimate view. - carPlayCameraPadding.bottom += 65.0 - - if geometryFramingAfterManeuver.enabled { - let nextStepIndex = min(activeState.currentLegStepIndex + 1, activeState.currentLegSteps.count - 1) - - var totalDistance: CLLocationDistance = 0.0 - for (index, step) in activeState.currentLegSteps.dropFirst(nextStepIndex).enumerated() { - guard let stepCoordinates = step.shape?.coordinates, - let distance = stepCoordinates.distance() else { continue } - - if index == 0 { - if distance >= geometryFramingAfterManeuver.distanceToFrameAfterManeuver { - let trimmedStepCoordinates = stepCoordinates - .trimmed(distance: geometryFramingAfterManeuver.distanceToFrameAfterManeuver) - compoundManeuvers.append(trimmedStepCoordinates) - break - } else { - compoundManeuvers.append(stepCoordinates) - totalDistance += distance - } - } else if distance >= 0.0, totalDistance < geometryFramingAfterManeuver - .distanceToCoalesceCompoundManeuvers - { - if distance + totalDistance >= geometryFramingAfterManeuver - .distanceToCoalesceCompoundManeuvers - { - let remanentDistance = geometryFramingAfterManeuver - .distanceToCoalesceCompoundManeuvers - totalDistance - let trimmedStepCoordinates = stepCoordinates.trimmed(distance: remanentDistance) - compoundManeuvers.append(trimmedStepCoordinates) - break - } else { - compoundManeuvers.append(stepCoordinates) - totalDistance += distance - } - } - } - } - - let coordinatesForManeuverFraming = compoundManeuvers.reduce([], +) - var coordinatesToFrame = coordinatesToManeuver.sliced( - from: nil, - to: LineString(coordinatesToManeuver).coordinateFromStart(distance: lookaheadDistance) - ) - let pitchNearManeuver = followingCameraOptions.pitchNearManeuver - if pitchNearManeuver.enabled, - activeState.distanceRemainingOnStep <= pitchNearManeuver.triggerDistanceToManeuver - { - coordinatesToFrame += coordinatesForManeuverFraming - } - - if options.followingCameraOptions.centerUpdatesAllowed || followingCamera.center == nil { - var center = location.coordinate - if let boundingBox = BoundingBox(from: coordinatesToFrame) { - let coordinates = [ - center, - [boundingBox.northEast, boundingBox.southWest].centerCoordinate, - ] - - let centerLineString = LineString(coordinates) - let centerLineStringTotalDistance = centerLineString.distance() ?? 0.0 - let centerCoordDistance = centerLineStringTotalDistance * (1 - pitchСoefficient) - if let adjustedCenter = centerLineString.coordinateFromStart(distance: centerCoordDistance) { - center = adjustedCenter - } - } - - newOptions.center = center - } - - if options.followingCameraOptions.zoomUpdatesAllowed || followingCamera.zoom == nil { - let defaultZoomLevel = 12.0 - let followingCarPlayCameraZoom = zoom( - coordinatesToFrame, - mapView: mapView, - pitch: pitch, - maxPitch: followingCameraOptions.defaultPitch, - edgeInsets: carPlayCameraPadding, - defaultZoomLevel: defaultZoomLevel, - maxZoomLevel: followingCameraOptions.zoomRange.upperBound, - minZoomLevel: followingCameraOptions.zoomRange.lowerBound - ) - newOptions.zoom = followingCarPlayCameraZoom - } - - if options.followingCameraOptions.bearingUpdatesAllowed || followingCamera.bearing == nil { - var bearing = location.course - let distance = fmax( - lookaheadDistance, - geometryFramingAfterManeuver.enabled - ? geometryFramingAfterManeuver.distanceToCoalesceCompoundManeuvers - : 0.0 - ) - let coordinatesForIntersections = coordinatesToManeuver.sliced( - from: nil, - to: LineString(coordinatesToManeuver) - .coordinateFromStart(distance: distance) - ) - - bearing = self.bearing( - location.course, - mapView: mapView, - coordinatesToManeuver: coordinatesForIntersections - ) - newOptions.bearing = bearing - } - - let followingCarPlayCameraAnchor = anchor( - pitchСoefficient, - bounds: mapView.bounds, - edgeInsets: carPlayCameraPadding - ) - - newOptions.anchor = followingCarPlayCameraAnchor - - if options.followingCameraOptions.pitchUpdatesAllowed || followingCamera.pitch == nil { - newOptions.pitch = CGFloat(pitch) - } - - if options.followingCameraOptions.paddingUpdatesAllowed || followingCamera.padding == nil { - if mapView.window?.screen.traitCollection.userInterfaceIdiom == .carPlay { - newOptions.padding = UIEdgeInsets( - top: followingCarPlayCameraAnchor.y, - left: carPlayCameraPadding.left, - bottom: mapView.bounds - .height - followingCarPlayCameraAnchor.y + 1.0, - right: carPlayCameraPadding.right - ) - } else { - newOptions.padding = carPlayCameraPadding - } - } - } - return newOptions - } - - private func newOverviewCamera(with state: ViewportDataSourceState) -> CameraOptions { - guard let mapView else { return .init() } - - // In active guidance navigation, camera in overview mode is relevant, during free-drive - // navigation it's not used. - guard case .active(let activeState) = state.navigationState else { return overviewCamera } - - var newOptions = currentNavigationCameraOptions.overviewCamera - let remainingCoordinatesOnRoute = activeState.remainingCoordinatesOnRoute - - let carPlayCameraPadding = mapView.safeAreaInsets + UIEdgeInsets.centerEdgeInsets - let overviewCameraOptions = options.overviewCameraOptions - - if overviewCameraOptions.pitchUpdatesAllowed || overviewCamera.pitch == nil { - newOptions.pitch = 0.0 - } - - if overviewCameraOptions.centerUpdatesAllowed || overviewCamera.center == nil { - if let boundingBox = BoundingBox(from: remainingCoordinatesOnRoute) { - let center = [ - boundingBox.southWest, - boundingBox.northEast, - ].centerCoordinate - - newOptions.center = center - } - } - - newOptions.anchor = anchor( - bounds: mapView.bounds, - edgeInsets: carPlayCameraPadding - ) - - if overviewCameraOptions.bearingUpdatesAllowed || overviewCamera.bearing == nil { - // In case if `NavigationCamera` is already in ``NavigationCameraState/overview`` value - // of bearing will be also ignored. - newOptions.bearing = 0.0 - } - - if overviewCameraOptions.zoomUpdatesAllowed || overviewCamera.zoom == nil { - newOptions.zoom = overviewCameraZoom( - remainingCoordinatesOnRoute, - mapView: mapView, - pitch: newOptions.pitch, - bearing: newOptions.bearing, - edgeInsets: carPlayCameraPadding, - maxZoomLevel: overviewCameraOptions.maximumZoomLevel - ) - } - - if overviewCameraOptions.paddingUpdatesAllowed || overviewCamera.padding == nil { - newOptions.padding = carPlayCameraPadding - } - return newOptions - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CommonViewportDataSource.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CommonViewportDataSource.swift deleted file mode 100644 index a1e353da2..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/CommonViewportDataSource.swift +++ /dev/null @@ -1,80 +0,0 @@ -import Combine -import CoreLocation -import MapboxDirections -import MapboxMaps -import Turf -import UIKit - -@MainActor -class CommonViewportDataSource { - var navigationCameraOptions: AnyPublisher { - _navigationCameraOptions.eraseToAnyPublisher() - } - - private var _navigationCameraOptions: CurrentValueSubject = .init(.init()) - - var currentNavigationCameraOptions: NavigationCameraOptions { - get { - _navigationCameraOptions.value - } - - set { - _navigationCameraOptions.value = newValue - } - } - - var options: NavigationViewportDataSourceOptions = .init() - - weak var mapView: MapView? - - private var lifetimeSubscriptions: Set = [] - - private let viewportParametersProvider: ViewportParametersProvider - - private var previousViewportParameters: ViewportDataSourceState? - private var workQueue: DispatchQueue = .init( - label: "com.mapbox.navigation.camera", - qos: .userInteractive, - autoreleaseFrequency: .workItem - ) - - // MARK: Initializer Methods - - required init(_ mapView: MapView) { - self.mapView = mapView - self.viewportParametersProvider = .init() - } - - func update( - using viewportState: ViewportState, - updateClosure: @escaping (ViewportDataSourceState) -> NavigationCameraOptions? - ) { - Task { @MainActor [weak self] in - guard let self else { return } - let viewportParameters = await viewportParameters(with: viewportState) - guard viewportParameters != previousViewportParameters else { return } - - previousViewportParameters = viewportParameters - if let newOptions = updateClosure(viewportParameters) { - _navigationCameraOptions.send(newOptions) - } - } - } - - private func viewportParameters(with viewportState: ViewportState) async -> ViewportDataSourceState { - await withUnsafeContinuation { continuation in - let options = options - let provider = viewportParametersProvider - workQueue.async { - let parameters = provider.parameters( - with: viewportState.location, - heading: viewportState.heading, - routeProgress: viewportState.routeProgress, - viewportPadding: viewportState.viewportPadding, - options: options - ) - continuation.resume(returning: parameters) - } - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/MobileViewportDataSource.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/MobileViewportDataSource.swift deleted file mode 100644 index ac00fa6d0..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/MobileViewportDataSource.swift +++ /dev/null @@ -1,341 +0,0 @@ -import Combine -import CoreLocation -import MapboxDirections -import MapboxMaps -import Turf -import UIKit - -/// The class, which conforms to ``ViewportDataSource`` protocol and provides default implementation of it for iOS. -@MainActor -public class MobileViewportDataSource: ViewportDataSource { - private var commonDataSource: CommonViewportDataSource - - weak var mapView: MapView? - - /// Initializes ``MobileViewportDataSource`` instance. - /// - Parameter mapView: An instance of `MapView`, which is going to be used for viewport calculation. `MapView` - /// will be weakly stored by ``CarPlayViewportDataSource``. - public required init(_ mapView: MapView) { - self.mapView = mapView - self.commonDataSource = .init(mapView) - } - - /// Options, which give the ability to control whether certain `CameraOptions` will be generated. - public var options: NavigationViewportDataSourceOptions { - get { commonDataSource.options } - set { commonDataSource.options = newValue } - } - - /// Notifies that the navigation camera options have changed in response to a viewport change or a manual - /// change via ``currentNavigationCameraOptions``. - public var navigationCameraOptions: AnyPublisher { - commonDataSource.navigationCameraOptions - } - - /// The last calculated or set manually ``NavigationCameraOptions``. - /// - /// You can disable calculation of specific properties by changing ``options`` and setting a desired value directly. - /// - /// For example, setting a zoom level manually for the following camera state will require: - /// 1. Setting ``FollowingCameraOptions/zoomUpdatesAllowed`` to `false`. - /// 2. Updating `zoom` of `CameraOptions` to a desired value. - /// - /// > Important: If you don't disable calculation, the value that is set manually will be overriden. - public var currentNavigationCameraOptions: NavigationCameraOptions { - get { - commonDataSource.currentNavigationCameraOptions - } - - set { - commonDataSource.currentNavigationCameraOptions = newValue - } - } - - /// Updates ``NavigationCameraOptions`` accoridng to the navigation state. - /// - Parameters: - /// - viewportState: The current viewport state. - public func update(using viewportState: ViewportState) { - commonDataSource.update(using: viewportState) { [weak self] state in - guard let self else { return nil } - return NavigationCameraOptions( - followingCamera: newFollowingCamera(with: state), - overviewCamera: newOverviewCamera(with: state) - ) - } - } - - private func newFollowingCamera(with state: ViewportDataSourceState) -> CameraOptions { - guard let mapView else { return .init() } - - let followingCameraOptions = options.followingCameraOptions - let viewportPadding = state.viewportPadding - var newOptions = currentNavigationCameraOptions.followingCamera - - if let location = state.location, state.navigationState.isInPassiveNavigationOrCompletedActive { - if followingCameraOptions.centerUpdatesAllowed || followingCamera.center == nil { - newOptions.center = location.coordinate - } - - if followingCameraOptions.zoomUpdatesAllowed || followingCamera.zoom == nil { - let altitude = 1700.0 - let zoom = CGFloat(ZoomLevelForAltitude( - altitude, - mapView.mapboxMap.cameraState.pitch, - location.coordinate.latitude, - mapView.bounds.size - )) - - newOptions.zoom = zoom - } - - if followingCameraOptions.bearingUpdatesAllowed || followingCamera.bearing == nil { - if followingCameraOptions.followsLocationCourse { - newOptions.bearing = location.course - } else { - newOptions.bearing = 0.0 - } - } - - newOptions.anchor = mapView.center - - if followingCameraOptions.pitchUpdatesAllowed || followingCamera.pitch == nil { - newOptions.pitch = 0.0 - } - - if followingCameraOptions.paddingUpdatesAllowed || followingCamera.padding == nil { - newOptions.padding = mapView.safeAreaInsets - } - - return newOptions - } - - if let location = state.location, case .active(let activeState) = state.navigationState, - !activeState.isRouteComplete - { - let coordinatesToManeuver = activeState.coordinatesToManeuver - let lookaheadDistance = activeState.lookaheadDistance - - var compoundManeuvers: [[CLLocationCoordinate2D]] = [] - let geometryFramingAfterManeuver = followingCameraOptions.geometryFramingAfterManeuver - let pitchСoefficient = pitchСoefficient( - distanceRemainingOnStep: activeState.distanceRemainingOnStep, - currentCoordinate: location.coordinate, - currentLegStepIndex: activeState.currentLegStepIndex, - currentLegSteps: activeState.currentLegSteps - ) - let pitch = followingCameraOptions.defaultPitch * pitchСoefficient - - if geometryFramingAfterManeuver.enabled { - let nextStepIndex = min(activeState.currentLegStepIndex + 1, activeState.currentLegSteps.count - 1) - - var totalDistance: CLLocationDistance = 0.0 - for (index, step) in activeState.currentLegSteps.dropFirst(nextStepIndex).enumerated() { - guard let stepCoordinates = step.shape?.coordinates, - let distance = stepCoordinates.distance() else { continue } - - if index == 0 { - if distance >= geometryFramingAfterManeuver.distanceToFrameAfterManeuver { - let trimmedStepCoordinates = stepCoordinates - .trimmed(distance: geometryFramingAfterManeuver.distanceToFrameAfterManeuver) - compoundManeuvers.append(trimmedStepCoordinates) - break - } else { - compoundManeuvers.append(stepCoordinates) - totalDistance += distance - } - } else if distance >= 0.0, totalDistance < geometryFramingAfterManeuver - .distanceToCoalesceCompoundManeuvers - { - if distance + totalDistance >= geometryFramingAfterManeuver - .distanceToCoalesceCompoundManeuvers - { - let remanentDistance = geometryFramingAfterManeuver - .distanceToCoalesceCompoundManeuvers - totalDistance - let trimmedStepCoordinates = stepCoordinates.trimmed(distance: remanentDistance) - compoundManeuvers.append(trimmedStepCoordinates) - break - } else { - compoundManeuvers.append(stepCoordinates) - totalDistance += distance - } - } - } - } - - let coordinatesForManeuverFraming = compoundManeuvers.reduce([], +) - var coordinatesToFrame = coordinatesToManeuver.sliced( - from: nil, - to: LineString(coordinatesToManeuver).coordinateFromStart(distance: lookaheadDistance) - ) - let pitchNearManeuver = followingCameraOptions.pitchNearManeuver - if pitchNearManeuver.enabled, - activeState.distanceRemainingOnStep <= pitchNearManeuver.triggerDistanceToManeuver - { - coordinatesToFrame += coordinatesForManeuverFraming - } - - if options.followingCameraOptions.centerUpdatesAllowed || followingCamera.center == nil { - var center = location.coordinate - if let boundingBox = BoundingBox(from: coordinatesToFrame) { - let coordinates = [ - center, - [boundingBox.northEast, boundingBox.southWest].centerCoordinate, - ] - - let centerLineString = LineString(coordinates) - let centerLineStringTotalDistance = centerLineString.distance() ?? 0.0 - let centerCoordDistance = centerLineStringTotalDistance * (1 - pitchСoefficient) - if let adjustedCenter = centerLineString.coordinateFromStart(distance: centerCoordDistance) { - center = adjustedCenter - } - } - - newOptions.center = center - } - - if options.followingCameraOptions.zoomUpdatesAllowed || followingCamera.zoom == nil { - let defaultZoomLevel = 12.0 - let followingMobileCameraZoom = zoom( - coordinatesToFrame, - mapView: mapView, - pitch: pitch, - maxPitch: followingCameraOptions.defaultPitch, - edgeInsets: viewportPadding, - defaultZoomLevel: defaultZoomLevel, - maxZoomLevel: followingCameraOptions.zoomRange.upperBound, - minZoomLevel: followingCameraOptions.zoomRange.lowerBound - ) - - newOptions.zoom = followingMobileCameraZoom - } - - if options.followingCameraOptions.bearingUpdatesAllowed || followingCamera.bearing == nil { - var bearing = location.course - let distance = fmax( - lookaheadDistance, - geometryFramingAfterManeuver.enabled - ? geometryFramingAfterManeuver.distanceToCoalesceCompoundManeuvers - : 0.0 - ) - let coordinatesForIntersections = coordinatesToManeuver.sliced( - from: nil, - to: LineString(coordinatesToManeuver) - .coordinateFromStart(distance: distance) - ) - - bearing = self.bearing( - location.course, - mapView: mapView, - coordinatesToManeuver: coordinatesForIntersections - ) - - var headingDirection: CLLocationDirection? - let isWalking = activeState.transportType == .walking - if isWalking { - if let trueHeading = state.heading?.trueHeading, trueHeading >= 0 { - headingDirection = trueHeading - } else if let magneticHeading = state.heading?.magneticHeading, magneticHeading >= 0 { - headingDirection = magneticHeading - } else { - headingDirection = bearing - } - } - - newOptions.bearing = !isWalking ? bearing : headingDirection - } - - let followingMobileCameraAnchor = anchor( - pitchСoefficient, - bounds: mapView.bounds, - edgeInsets: viewportPadding - ) - - newOptions.anchor = followingMobileCameraAnchor - - if options.followingCameraOptions.pitchUpdatesAllowed || followingCamera.pitch == nil { - newOptions.pitch = CGFloat(pitch) - } - - if options.followingCameraOptions.paddingUpdatesAllowed || followingCamera.padding == nil { - newOptions.padding = UIEdgeInsets( - top: followingMobileCameraAnchor.y, - left: viewportPadding.left, - bottom: mapView.bounds.height - followingMobileCameraAnchor - .y + 1.0, - right: viewportPadding.right - ) - } - } - return newOptions - } - - private func newOverviewCamera(with state: ViewportDataSourceState) -> CameraOptions { - guard let mapView else { return .init() } - - // In active guidance navigation, camera in overview mode is relevant, during free-drive - // navigation it's not used. - guard case .active(let activeState) = state.navigationState else { return overviewCamera } - - var newOptions = currentNavigationCameraOptions.overviewCamera - let remainingCoordinatesOnRoute = activeState.remainingCoordinatesOnRoute - let viewportPadding = state.viewportPadding - - let overviewCameraOptions = options.overviewCameraOptions - - if overviewCameraOptions.pitchUpdatesAllowed || overviewCamera.pitch == nil { - newOptions.pitch = 0.0 - } - - if overviewCameraOptions.centerUpdatesAllowed || overviewCamera.center == nil { - if let boundingBox = BoundingBox(from: remainingCoordinatesOnRoute) { - let center = [ - boundingBox.southWest, - boundingBox.northEast, - ].centerCoordinate - - newOptions.center = center - } - } - - newOptions.anchor = anchor( - bounds: mapView.bounds, - edgeInsets: viewportPadding - ) - - if overviewCameraOptions.bearingUpdatesAllowed || overviewCamera.bearing == nil { - // In case if `NavigationCamera` is already in ``NavigationCameraState/overview`` value - // of bearing will be also ignored. - let bearing = 0.0 - - var headingDirection: CLLocationDirection? - let isWalking = activeState.transportType == .walking - if isWalking { - if let trueHeading = state.heading?.trueHeading, trueHeading >= 0 { - headingDirection = trueHeading - } else if let magneticHeading = state.heading?.magneticHeading, magneticHeading >= 0 { - headingDirection = magneticHeading - } else { - headingDirection = bearing - } - } - - newOptions.bearing = !isWalking ? bearing : headingDirection - } - - if overviewCameraOptions.zoomUpdatesAllowed || overviewCamera.zoom == nil { - newOptions.zoom = overviewCameraZoom( - remainingCoordinatesOnRoute, - mapView: mapView, - pitch: newOptions.pitch, - bearing: newOptions.bearing, - edgeInsets: viewportPadding, - maxZoomLevel: overviewCameraOptions.maximumZoomLevel - ) - } - - if overviewCameraOptions.paddingUpdatesAllowed || overviewCamera.padding == nil { - newOptions.padding = viewportPadding - } - return newOptions - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource+Calculation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource+Calculation.swift deleted file mode 100644 index f7b99d289..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource+Calculation.swift +++ /dev/null @@ -1,146 +0,0 @@ -import CoreLocation -import MapboxDirections -import MapboxMaps -import Turf -import UIKit - -extension ViewportDataSource { - func bearing( - _ initialBearing: CLLocationDirection, - mapView: MapView?, - coordinatesToManeuver: [CLLocationCoordinate2D]? = nil - ) -> CLLocationDirection { - var bearing = initialBearing - - if let coordinates = coordinatesToManeuver, - let firstCoordinate = coordinates.first, - let lastCoordinate = coordinates.last - { - let directionToManeuver = firstCoordinate.direction(to: lastCoordinate) - let directionDiff = directionToManeuver.shortestRotation(angle: initialBearing) - let bearingSmoothing = options.followingCameraOptions.bearingSmoothing - let bearingMaxDiff = bearingSmoothing.enabled ? bearingSmoothing.maximumBearingSmoothingAngle : 0.0 - if fabs(directionDiff) > bearingMaxDiff { - bearing += bearingMaxDiff * (directionDiff < 0.0 ? -1.0 : 1.0) - } else { - bearing = firstCoordinate.direction(to: lastCoordinate) - } - } - - let mapViewBearing = Double(mapView?.mapboxMap.cameraState.bearing ?? 0.0) - return mapViewBearing + bearing.shortestRotation(angle: mapViewBearing) - } - - func zoom( - _ coordinates: [CLLocationCoordinate2D], - mapView: MapView?, - pitch: Double = 0.0, - maxPitch: Double = 0.0, - edgeInsets: UIEdgeInsets = .zero, - defaultZoomLevel: Double = 12.0, - maxZoomLevel: Double = 22.0, - minZoomLevel: Double = 2.0 - ) -> CGFloat { - guard let mapView, - let boundingBox = BoundingBox(from: coordinates) else { return CGFloat(defaultZoomLevel) } - - let mapViewInsetWidth = mapView.bounds.size.width - edgeInsets.left - edgeInsets.right - let mapViewInsetHeight = mapView.bounds.size.height - edgeInsets.top - edgeInsets.bottom - let widthDelta = mapViewInsetHeight * 2 - mapViewInsetWidth - let pitchDelta = CGFloat(pitch / maxPitch) * widthDelta - let widthWithPitchEffect = CGFloat(mapViewInsetWidth + CGFloat(pitchDelta.isNaN ? 0.0 : pitchDelta)) - let heightWithPitchEffect = - CGFloat(mapViewInsetHeight + mapViewInsetHeight * CGFloat(sin(pitch * .pi / 180.0)) * 1.25) - let zoomLevel = boundingBox.zoomLevel(fitTo: CGSize(width: widthWithPitchEffect, height: heightWithPitchEffect)) - - return CGFloat(max(min(zoomLevel, maxZoomLevel), minZoomLevel)) - } - - func overviewCameraZoom( - _ coordinates: [CLLocationCoordinate2D], - mapView: MapView?, - pitch: CGFloat?, - bearing: CLLocationDirection?, - edgeInsets: UIEdgeInsets, - defaultZoomLevel: Double = 12.0, - maxZoomLevel: Double = 22.0, - minZoomLevel: Double = 2.0 - ) -> CGFloat { - guard let mapView else { return CGFloat(defaultZoomLevel) } - - let initialCameraOptions = CameraOptions( - padding: edgeInsets, - bearing: 0, - pitch: 0 - ) - guard let options = try? mapView.mapboxMap.camera( - for: coordinates, - camera: initialCameraOptions, - coordinatesPadding: nil, - maxZoom: nil, - offset: nil - ) else { - return CGFloat(defaultZoomLevel) - } - return CGFloat(max(min(options.zoom ?? defaultZoomLevel, maxZoomLevel), minZoomLevel)) - } - - func anchor( - _ pitchСoefficient: Double = 0.0, - bounds: CGRect = .zero, - edgeInsets: UIEdgeInsets = .zero - ) -> CGPoint { - let xCenter = max(((bounds.size.width - edgeInsets.left - edgeInsets.right) / 2.0) + edgeInsets.left, 0.0) - let height = (bounds.size.height - edgeInsets.top - edgeInsets.bottom) - let yCenter = max((height / 2.0) + edgeInsets.top, 0.0) - let yOffsetCenter = max((height / 2.0) - 7.0, 0.0) * CGFloat(pitchСoefficient) + yCenter - - return CGPoint(x: xCenter, y: yOffsetCenter) - } - - func pitchСoefficient( - distanceRemainingOnStep: CLLocationDistance, - currentCoordinate: CLLocationCoordinate2D, - currentLegStepIndex: Int, - currentLegSteps: [RouteStep] - ) -> Double { - let defaultPitchСoefficient = 1.0 - let pitchNearManeuver = options.followingCameraOptions.pitchNearManeuver - guard pitchNearManeuver.enabled else { return defaultPitchСoefficient } - - var shouldIgnoreManeuver = false - if let upcomingStep = currentLegSteps[safe: currentLegStepIndex + 1] { - if currentLegStepIndex == currentLegSteps.count - 2 { - shouldIgnoreManeuver = true - } - - let maneuvers: [ManeuverType] = [.continue, .merge, .takeOnRamp, .takeOffRamp, .reachFork] - if maneuvers.contains(upcomingStep.maneuverType) { - shouldIgnoreManeuver = true - } - } - - if distanceRemainingOnStep <= pitchNearManeuver.triggerDistanceToManeuver, !shouldIgnoreManeuver, - pitchNearManeuver.triggerDistanceToManeuver != 0.0 - { - return distanceRemainingOnStep / pitchNearManeuver.triggerDistanceToManeuver - } - return defaultPitchСoefficient - } - - var followingCamera: CameraOptions { - currentNavigationCameraOptions.followingCamera - } - - var overviewCamera: CameraOptions { - currentNavigationCameraOptions.overviewCamera - } -} - -extension ViewportDataSourceState.NavigationState { - var isInPassiveNavigationOrCompletedActive: Bool { - if case .passive = self { return true } - if case .active(let activeState) = self, activeState.isRouteComplete { return true } - return false - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource.swift deleted file mode 100644 index 9274ee980..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSource.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Combine -import CoreLocation -import MapboxMaps -import UIKit - -/// Represents the state of the viewport. -public struct ViewportState: Equatable, Sendable { - /// The current location of the user. - public let location: CLLocation - /// The navigation route progress. - public let routeProgress: RouteProgress? - /// The padding applied to the viewport. - public let viewportPadding: UIEdgeInsets - /// The current user heading. - public let heading: CLHeading? - - /// Initializes a new ``ViewportState`` instance. - /// - Parameters: - /// - location: The current location of the user. - /// - routeProgress: The navigation route progress. Pass `nil` in case of no active navigation at the moment. - /// - viewportPadding: The padding applied to the viewport. - /// - heading: The current user heading. - public init( - location: CLLocation, - routeProgress: RouteProgress?, - viewportPadding: UIEdgeInsets, - heading: CLHeading? - ) { - self.location = location - self.routeProgress = routeProgress - self.viewportPadding = viewportPadding - self.heading = heading - } -} - -/// The protocol, which is used to fill and store ``NavigationCameraOptions`` which will be used by ``NavigationCamera`` -/// for execution of transitions and continuous updates. -/// -/// By default Navigation SDK for iOS provides default implementation of ``ViewportDataSource`` in -/// ``MobileViewportDataSource`` and ``CarPlayViewportDataSource``. -@MainActor -public protocol ViewportDataSource: AnyObject { - /// Options, which give the ability to control whether certain `CameraOptions` will be generated. - var options: NavigationViewportDataSourceOptions { get } - - /// Notifies that the navigation camera options have changed in response to a viewport change. - var navigationCameraOptions: AnyPublisher { get } - - /// The last calculated ``NavigationCameraOptions``. - var currentNavigationCameraOptions: NavigationCameraOptions { get } - - /// Updates ``NavigationCameraOptions`` accoridng to the navigation state. - /// - Parameters: - /// - viewportState: The current viewport state. - func update(using viewportState: ViewportState) -} - -extension CLHeading: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSourceState.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSourceState.swift deleted file mode 100644 index 40e34cb2a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportDataSource/ViewportDataSourceState.swift +++ /dev/null @@ -1,28 +0,0 @@ -import CoreLocation -import Foundation -import MapboxDirections -import Turf -import UIKit - -struct ViewportDataSourceState: Equatable, Sendable { - enum NavigationState: Equatable, Sendable { - case passive - case active(ActiveNavigationState) - } - - struct ActiveNavigationState: Equatable, Sendable { - var coordinatesToManeuver: [LocationCoordinate2D] - var lookaheadDistance: LocationDistance - var currentLegStepIndex: Int - var currentLegSteps: [RouteStep] - var isRouteComplete: Bool - var remainingCoordinatesOnRoute: [LocationCoordinate2D] - var transportType: TransportType - var distanceRemainingOnStep: CLLocationDistance - } - - var location: CLLocation? - var heading: CLHeading? - var navigationState: NavigationState - var viewportPadding: UIEdgeInsets -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportParametersProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportParametersProvider.swift deleted file mode 100644 index 869957c15..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Camera/ViewportParametersProvider.swift +++ /dev/null @@ -1,100 +0,0 @@ -import CoreLocation -import Foundation -import MapboxDirections -import UIKit - -struct ViewportParametersProvider: Sendable { - func parameters( - with location: CLLocation?, - heading: CLHeading?, - routeProgress: RouteProgress?, - viewportPadding: UIEdgeInsets, - options: NavigationViewportDataSourceOptions - ) -> ViewportDataSourceState { - if let routeProgress { - let intersectionDensity = options.followingCameraOptions.intersectionDensity - let stepIndex = routeProgress.currentLegProgress.stepIndex - let nextStepIndex = min(stepIndex + 1, routeProgress.currentLeg.steps.count - 1) - - var remainingCoordinatesOnRoute = routeProgress.currentLegProgress.currentStepProgress - .remainingStepCoordinates() - routeProgress.currentLeg.steps[nextStepIndex...] - .lazy - .compactMap { $0.shape?.coordinates } - .forEach { stepCoordinates in - remainingCoordinatesOnRoute.append(contentsOf: stepCoordinates) - } - - return .init( - location: location, - heading: heading, - navigationState: .active( - .init( - coordinatesToManeuver: routeProgress.currentLegProgress.currentStepProgress - .remainingStepCoordinates(), - lookaheadDistance: lookaheadDistance(routeProgress, intersectionDensity: intersectionDensity), - currentLegStepIndex: routeProgress.currentLegProgress.stepIndex, - currentLegSteps: routeProgress.currentLeg.steps, - isRouteComplete: routeProgress.routeIsComplete == true, - remainingCoordinatesOnRoute: remainingCoordinatesOnRoute, - transportType: routeProgress.currentLegProgress.currentStep.transportType, - distanceRemainingOnStep: routeProgress.currentLegProgress.currentStepProgress.distanceRemaining - ) - ), - viewportPadding: viewportPadding - ) - } else { - return .init( - location: location, - navigationState: .passive, - viewportPadding: viewportPadding - ) - } - } - - /// Calculates lookahead distance based on current ``RouteProgress`` and ``IntersectionDensity`` coefficients. - /// Lookahead distance value will be influenced by both ``IntersectionDensity.minimumDistanceBetweenIntersections`` - /// and ``IntersectionDensity.averageDistanceMultiplier``. - /// - Parameters: - /// - routeProgress: Current `RouteProgress` - /// - intersectionDensity: Lookahead distance - /// - Returns: The lookahead distance. - private func lookaheadDistance( - _ routeProgress: RouteProgress, - intersectionDensity: IntersectionDensity - ) -> CLLocationDistance { - let averageIntersectionDistances = routeProgress.route.legs.map { leg -> [CLLocationDistance] in - return leg.steps.map { step -> CLLocationDistance in - if let firstStepCoordinate = step.shape?.coordinates.first, - let lastStepCoordinate = step.shape?.coordinates.last - { - let intersectionLocations = [firstStepCoordinate] + ( - step.intersections?.map(\.location) ?? [] - ) + - [lastStepCoordinate] - let intersectionDistances = intersectionLocations[1...].enumerated() - .map { index, intersection -> CLLocationDistance in - return intersection.distance(to: intersectionLocations[index]) - } - let filteredIntersectionDistances = intersectionDensity.enabled - ? intersectionDistances.filter { $0 > intersectionDensity.minimumDistanceBetweenIntersections } - : intersectionDistances - let averageIntersectionDistance = filteredIntersectionDistances - .reduce(0.0, +) / Double(filteredIntersectionDistances.count) - return averageIntersectionDistance - } - - return 0.0 - } - } - - let averageDistanceMultiplier = intersectionDensity.enabled ? intersectionDensity - .averageDistanceMultiplier : 1.0 - let currentRouteLegIndex = routeProgress.legIndex - let currentRouteStepIndex = routeProgress.currentLegProgress.stepIndex - let lookaheadDistance = averageIntersectionDistances[currentRouteLegIndex][currentRouteStepIndex] * - averageDistanceMultiplier - - return lookaheadDistance - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/MapPoint.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/MapPoint.swift deleted file mode 100644 index 6b06d0afb..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/MapPoint.swift +++ /dev/null @@ -1,16 +0,0 @@ -import CoreLocation - -/// Represents a point that user tapped on the map. -public struct MapPoint: Equatable, Sendable { - /// Name of the POI that user tapped on. Can be `nil` if there were no POIs nearby. - /// Developers can adjust ``NavigationMapView/poiClickableAreaSize`` - /// to increase the search area around the touch point. - public let name: String? - /// Coordinate of user's tap. - public let coordinate: CLLocationCoordinate2D - - public init(name: String?, coordinate: CLLocationCoordinate2D) { - self.name = name - self.coordinate = coordinate - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/MapView.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/MapView.swift deleted file mode 100644 index 622273797..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/MapView.swift +++ /dev/null @@ -1,205 +0,0 @@ -import CoreLocation -import Foundation -import MapboxDirections -import MapboxMaps - -private let trafficTileSetIdentifiers = Set([ - "mapbox.mapbox-traffic-v1", - "mapbox.mapbox-traffic-v2-beta", -]) - -private let incidentsTileSetIdentifiers = Set([ - "mapbox.mapbox-incidents-v1", - "mapbox.mapbox-incidents-v2-beta", -]) - -/// An extension on `MapView` that allows for toggling traffic on a map style that contains a [Mapbox Traffic -/// source](https://docs.mapbox.com/vector-tiles/mapbox-traffic-v1/). -extension MapView { - /// Returns a set of tile set identifiers for specific `sourceIdentifier`. - /// - /// - parameter sourceIdentifier: Identifier of the source, which will be searched for in current style of the - /// ///`MapView`. - /// - returns: Set of tile set identifiers. - func tileSetIdentifiers(_ sourceIdentifier: String) -> Set { - if let properties = try? mapboxMap.sourceProperties(for: sourceIdentifier), - let url = properties["url"] as? String, - let configurationURL = URL(string: url), - configurationURL.scheme == "mapbox", - let tileSetIdentifiers = configurationURL.host?.components(separatedBy: ",") - { - return Set(tileSetIdentifiers) - } - - return Set() - } - - /// Returns a list of identifiers of the tile sets that make up specific source type. - /// - /// This array contains multiple entries for a composited source. This property is empty for non-Mapbox-hosted tile - /// sets and sources with type other than `vector`. - /// - /// - parameter sourceIdentifier: Identifier of the source. - /// - parameter sourceType: Type of the source (e.g. `vector`). - /// - returns: List of tile set identifiers. - func tileSetIdentifiers(_ sourceIdentifier: String, sourceType: String) -> [String] { - if sourceType == "vector" { - return Array(tileSetIdentifiers(sourceIdentifier)) - } - - return [] - } - - /// Returns a set of source identifiers for tilesets that are or include the given source. - /// - /// - parameter tileSetIdentifier: Identifier of the tile set in the form `user.tileset`. - /// - returns: Set of source identifiers. - func sourceIdentifiers(_ tileSetIdentifiers: Set) -> Set { - return Set(mapboxMap.allSourceIdentifiers.filter { - $0.type.rawValue == "vector" - }.filter { - !self.tileSetIdentifiers($0.id).isDisjoint(with: tileSetIdentifiers) - }.map(\.id)) - } - - /// Returns a Boolean value indicating whether data from the given tile set layers is currently all visible in the - /// map view’s style. - /// - /// - parameter tileSetIdentifiers: Identifiers of the tile sets in the form `user.tileset`. - /// - parameter layerIdentifier: Identifier of the layer in the tile set; in other words, a source layer identifier. - /// Not to be confused with a style layer. - func showsTileSet(with tileSetIdentifiers: Set, layerIdentifier: String) -> Bool { - let sourceIdentifiers = sourceIdentifiers(tileSetIdentifiers) - var foundTileSets = false - - for mapViewLayerIdentifier in mapboxMap.allLayerIdentifiers.map(\.id) { - guard let sourceIdentifier = mapboxMap.layerProperty( - for: mapViewLayerIdentifier, - property: "source" - ).value as? String, - let sourceLayerIdentifier = mapboxMap.layerProperty( - for: mapViewLayerIdentifier, - property: "source-layer" - ).value as? String - else { return false } - - if sourceIdentifiers.contains(sourceIdentifier), sourceLayerIdentifier == layerIdentifier { - foundTileSets = true - let visibility = mapboxMap.layerProperty(for: mapViewLayerIdentifier, property: "visibility") - .value as? String - if visibility != "visible" { - return false - } - } - } - - return foundTileSets - } - - /// Shows or hides data from the given tile set layers. - /// - /// - parameter isVisible: Parameter, which controls whether layer should be visible or not. - /// - parameter tileSetIdentifiers: Identifiers of the tile sets in the form `user.tileset`. - /// - parameter layerIdentifier: Identifier of the layer in the tile set; in other words, a source layer identifier. - /// Not to be confused with a style layer. - func setShowsTileSet(_ isVisible: Bool, with tileSetIdentifiers: Set, layerIdentifier: String) { - let sourceIdentifiers = sourceIdentifiers(tileSetIdentifiers) - - for mapViewLayerIdentifier in mapboxMap.allLayerIdentifiers.map(\.id) { - guard let sourceIdentifier = mapboxMap.layerProperty( - for: mapViewLayerIdentifier, - property: "source" - ).value as? String, - let sourceLayerIdentifier = mapboxMap.layerProperty( - for: mapViewLayerIdentifier, - property: "source-layer" - ).value as? String - else { return } - - if sourceIdentifiers.contains(sourceIdentifier), sourceLayerIdentifier == layerIdentifier { - let properties = [ - "visibility": isVisible ? "visible" : "none", - ] - try? mapboxMap.setLayerProperties(for: mapViewLayerIdentifier, properties: properties) - } - } - } - - /// A Boolean value indicating whether traffic congestion lines are visible in the map view’s style. - var showsTraffic: Bool { - get { - return showsTileSet(with: trafficTileSetIdentifiers, layerIdentifier: "traffic") - } - set { - setShowsTileSet(newValue, with: trafficTileSetIdentifiers, layerIdentifier: "traffic") - } - } - - /// A Boolean value indicating whether incidents, such as road closures and detours, are visible in the map view’s - /// style. - var showsIncidents: Bool { - get { - return showsTileSet(with: incidentsTileSetIdentifiers, layerIdentifier: "closures") - } - set { - setShowsTileSet(newValue, with: incidentsTileSetIdentifiers, layerIdentifier: "closures") - } - } - - /// Method, which returns list of source identifiers, which contain streets tile set. - func streetsSources() -> [SourceInfo] { - return mapboxMap.allSourceIdentifiers.filter { - let identifiers = tileSetIdentifiers($0.id, sourceType: $0.type.rawValue) - return VectorSource.isMapboxStreets(identifiers) - } - } - - /// Attempts to localize road labels into the local language and other labels into the given locale. - func localizeLabels(into locale: Locale) { - guard let mapboxStreetsSource = streetsSources().first else { return } - - let streetsSourceTilesetIdentifiers = tileSetIdentifiers(mapboxStreetsSource.id) - let roadLabelSourceLayerIdentifier = streetsSourceTilesetIdentifiers - .compactMap { VectorSource.roadLabelLayerIdentifiersByTileSetIdentifier[$0] - }.first - - let localizableLayerIdentifiers = mapboxMap.allLayerIdentifiers.lazy - .filter { - $0.type == .symbol - } - // We only know how to localize layers backed by the Mapbox Streets source. - .filter { - self.mapboxMap.layerProperty(for: $0.id, property: "source").value as? String == mapboxStreetsSource.id - } - // Road labels should match road signage, so they should not be localized. - // TODO: Actively delocalize road labels into the “name” property: https://github.com/mapbox/mapbox-maps-ios/issues/653 - .filter { - self.mapboxMap.layerProperty( - for: $0.id, - property: "source-layer" - ).value as? String != roadLabelSourceLayerIdentifier - } - .map(\.id) - try? mapboxMap.localizeLabels(into: locale, forLayerIds: Array(localizableLayerIdentifiers)) - } -} - -extension MapView { - /// Returns a tileset descriptor for current map style. - /// - /// - parameter zoomRange: Closed range zoom level for the tile package. - /// - returns: A tileset descriptor. - func tilesetDescriptor(zoomRange: ClosedRange) -> TilesetDescriptor? { - guard let styleURI = mapboxMap.styleURI, - URL(string: styleURI.rawValue)?.scheme == "mapbox" - else { return nil } - - let offlineManager = OfflineManager() - let tilesetDescriptorOptions = TilesetDescriptorOptions( - styleURI: styleURI, - zoomRange: zoomRange, - tilesets: nil - ) - return offlineManager.createTilesetDescriptor(for: tilesetDescriptorOptions) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+ContinuousAlternatives.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+ContinuousAlternatives.swift deleted file mode 100644 index 2a533cf93..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+ContinuousAlternatives.swift +++ /dev/null @@ -1,64 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import MapboxDirections -import Turf -import UIKit - -extension NavigationMapView { - /// Returns a list of the ``AlternativeRoute``s, that are close to a certain point and are within threshold distance - /// defined in ``NavigationMapView/tapGestureDistanceThreshold``. - /// - /// - parameter point: Point on the screen. - /// - returns: List of the alternative routes, which were found. If there are no continuous alternatives routes on - /// the map view `nil` will be returned. - /// An empty array is returned if no alternative route was tapped or if there are multiple equally fitting - /// routes at the tap coordinate. - func continuousAlternativeRoutes(closeTo point: CGPoint) -> [AlternativeRoute]? { - guard let routes, !routes.alternativeRoutes.isEmpty - else { - return nil - } - - // Workaround for XCode 12.5 compilation bug - typealias RouteWithMetadata = (route: Route, index: Int, distance: LocationDistance) - - let continuousAlternatives = routes.alternativeRoutes - // Add the main route to detect if the main route is the closest to the point. The main route is excluded from - // the result array. - let allRoutes = [routes.mainRoute.route] + continuousAlternatives.map { $0.route } - - // Filter routes with at least 2 coordinates and within tap distance. - let tapCoordinate = mapView.mapboxMap.coordinate(for: point) - let routeMetadata = allRoutes.enumerated() - .compactMap { index, route -> RouteWithMetadata? in - guard route.shape?.coordinates.count ?? 0 > 1 else { - return nil - } - guard let closestCoordinate = route.shape?.closestCoordinate(to: tapCoordinate)?.coordinate else { - return nil - } - - let closestPoint = mapView.mapboxMap.point(for: closestCoordinate) - guard closestPoint.distance(to: point) < tapGestureDistanceThreshold else { - return nil - } - let distance = closestCoordinate.distance(to: tapCoordinate) - return RouteWithMetadata(route: route, index: index, distance: distance) - } - - // Sort routes by closest distance to tap gesture. - let closest = routeMetadata.sorted { (lhs: RouteWithMetadata, rhs: RouteWithMetadata) -> Bool in - return lhs.distance < rhs.distance - } - - // Exclude the routes if the distance is the same and we cannot distinguish the routes. - if routeMetadata.count > 1, abs(routeMetadata[0].distance - routeMetadata[1].distance) < 1e-6 { - return [] - } - - return closest.compactMap { (item: RouteWithMetadata) -> AlternativeRoute? in - guard item.index > 0 else { return nil } - return continuousAlternatives[item.index - 1] - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+Gestures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+Gestures.swift deleted file mode 100644 index 955badd75..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+Gestures.swift +++ /dev/null @@ -1,206 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import MapboxDirections -import MapboxMaps -import Turf -import UIKit - -extension NavigationMapView { - func setupGestureRecognizers() { - // Gesture recognizer, which is used to detect long taps on any point on the map. - let longPressGestureRecognizer = UILongPressGestureRecognizer( - target: self, - action: #selector(handleLongPress(_:)) - ) - addGestureRecognizer(longPressGestureRecognizer) - - // Gesture recognizer, which is used to detect taps on route line, waypoint or POI - mapViewTapGestureRecognizer = UITapGestureRecognizer( - target: self, - action: #selector(didReceiveTap(gesture:)) - ) - mapViewTapGestureRecognizer.delegate = self - mapView.addGestureRecognizer(mapViewTapGestureRecognizer) - - makeGestureRecognizersDisableCameraFollowing() - makeTapGestureRecognizerStopAnimatedTransitions() - } - - @objc - private func handleLongPress(_ gesture: UIGestureRecognizer) { - guard gesture.state == .began else { return } - let gestureLocation = gesture.location(in: self) - Task { @MainActor in - let point = await mapPoint(at: gestureLocation) - delegate?.navigationMapView(self, userDidLongTap: point) - } - } - - /// Modifies `MapView` gesture recognizers to disable follow mode and move `NavigationCamera` to - /// `NavigationCameraState.idle` state. - private func makeGestureRecognizersDisableCameraFollowing() { - for gestureRecognizer in mapView.gestureRecognizers ?? [] - where gestureRecognizer is UIPanGestureRecognizer - || gestureRecognizer is UIRotationGestureRecognizer - || gestureRecognizer is UIPinchGestureRecognizer - || gestureRecognizer == mapView.gestures.doubleTapToZoomInGestureRecognizer - || gestureRecognizer == mapView.gestures.doubleTouchToZoomOutGestureRecognizer - - { - gestureRecognizer.addTarget(self, action: #selector(switchToIdleCamera)) - } - } - - private func makeTapGestureRecognizerStopAnimatedTransitions() { - for gestureRecognizer in mapView.gestureRecognizers ?? [] - where gestureRecognizer is UITapGestureRecognizer - && gestureRecognizer != mapView.gestures.doubleTouchToZoomOutGestureRecognizer - { - gestureRecognizer.addTarget(self, action: #selector(switchToIdleCameraIfNotFollowing)) - } - } - - @objc - private func switchToIdleCamera() { - update(navigationCameraState: .idle) - } - - @objc - private func switchToIdleCameraIfNotFollowing() { - guard navigationCamera.currentCameraState != .following else { return } - update(navigationCameraState: .idle) - } - - /// Fired when NavigationMapView detects a tap not handled elsewhere by other gesture recognizers. - @objc - private func didReceiveTap(gesture: UITapGestureRecognizer) { - guard gesture.state == .recognized else { return } - let tapPoint = gesture.location(in: mapView) - - Task { - if let allRoutes = routes?.allRoutes() { - let waypointTest = legSeparatingWaypoints(on: allRoutes, closeTo: tapPoint) - if let selected = waypointTest?.first { - delegate?.navigationMapView(self, didSelect: selected) - return - } - } - - if let alternativeRoute = continuousAlternativeRoutes(closeTo: tapPoint)?.first { - delegate?.navigationMapView(self, didSelect: alternativeRoute) - return - } - - let point = await mapPoint(at: tapPoint) - - if point.name != nil { - delegate?.navigationMapView(self, userDidTap: point) - } - } - } - - func legSeparatingWaypoints(on routes: [Route], closeTo point: CGPoint) -> [Waypoint]? { - // In case if route does not contain more than one leg - do nothing. - let multipointRoutes = routes.filter { $0.legs.count > 1 } - guard multipointRoutes.count > 0 else { return nil } - - let waypoints = multipointRoutes.compactMap { route in - route.legs.dropLast().compactMap { $0.destination } - }.flatMap { $0 } - - // Sort the array in order of closest to tap. - let tapCoordinate = mapView.mapboxMap.coordinate(for: point) - let closest = waypoints.sorted { left, right -> Bool in - let leftDistance = left.coordinate.projectedDistance(to: tapCoordinate) - let rightDistance = right.coordinate.projectedDistance(to: tapCoordinate) - return leftDistance < rightDistance - } - - // Filter to see which ones are under threshold. - let candidates = closest.filter { - let coordinatePoint = mapView.mapboxMap.point(for: $0.coordinate) - - return coordinatePoint.distance(to: point) < tapGestureDistanceThreshold - } - - return candidates - } - - private func mapPoint(at point: CGPoint) async -> MapPoint { - let options = RenderedQueryOptions(layerIds: mapStyleManager.poiLayerIds, filter: nil) - let rectSize = poiClickableAreaSize - let rect = CGRect(x: point.x - rectSize / 2, y: point.y - rectSize / 2, width: rectSize, height: rectSize) - - let features = try? await mapView.mapboxMap.queryRenderedFeatures(with: rect, options: options) - if let feature = features?.first?.queriedFeature.feature, - case .string(let poiName) = feature[property: .poiName, languageCode: nil], - case .point(let point) = feature.geometry - { - return MapPoint(name: poiName, coordinate: point.coordinates) - } else { - let coordinate = mapView.mapboxMap.coordinate(for: point) - return MapPoint(name: nil, coordinate: coordinate) - } - } -} - -// MARK: - GestureManagerDelegate - -extension NavigationMapView: GestureManagerDelegate { - public nonisolated func gestureManager( - _ gestureManager: MapboxMaps.GestureManager, - didBegin gestureType: MapboxMaps.GestureType - ) { - guard gestureType != .singleTap else { return } - - MainActor.assumingIsolated { - delegate?.navigationMapViewUserDidStartInteraction(self) - } - } - - public nonisolated func gestureManager( - _ gestureManager: MapboxMaps.GestureManager, - didEnd gestureType: MapboxMaps.GestureType, - willAnimate: Bool - ) { - guard gestureType != .singleTap else { return } - - MainActor.assumingIsolated { - delegate?.navigationMapViewUserDidEndInteraction(self) - } - } - - public nonisolated func gestureManager( - _ gestureManager: MapboxMaps.GestureManager, - didEndAnimatingFor gestureType: MapboxMaps.GestureType - ) {} -} - -// MARK: - UIGestureRecognizerDelegate - -extension NavigationMapView: UIGestureRecognizerDelegate { - public func gestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer - ) -> Bool { - if gestureRecognizer is UITapGestureRecognizer, - otherGestureRecognizer is UITapGestureRecognizer - { - return true - } - - return false - } - - public func gestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer - ) -> Bool { - if gestureRecognizer is UITapGestureRecognizer, - otherGestureRecognizer == mapView.gestures.doubleTapToZoomInGestureRecognizer - { - return true - } - return false - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+VanishingRouteLine.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+VanishingRouteLine.swift deleted file mode 100644 index dc174a80a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView+VanishingRouteLine.swift +++ /dev/null @@ -1,212 +0,0 @@ -import _MapboxNavigationHelpers -import CoreLocation -import MapboxDirections -import MapboxMaps -import UIKit - -extension NavigationMapView { - struct RoutePoints { - var nestedList: [[[CLLocationCoordinate2D]]] - var flatList: [CLLocationCoordinate2D] - } - - struct RouteLineGranularDistances { - var distance: Double - var distanceArray: [RouteLineDistancesIndex] - } - - struct RouteLineDistancesIndex { - var point: CLLocationCoordinate2D - var distanceRemaining: Double - } - - // MARK: Customizing and Displaying the Route Line(s) - - func initPrimaryRoutePoints(route: Route) { - routePoints = parseRoutePoints(route: route) - routeLineGranularDistances = calculateGranularDistances(routePoints?.flatList ?? []) - } - - /// Transform the route data into nested arrays of legs -> steps -> coordinates. - /// The first and last point of adjacent steps overlap and are duplicated. - func parseRoutePoints(route: Route) -> RoutePoints { - let nestedList = route.legs.map { (routeLeg: RouteLeg) -> [[CLLocationCoordinate2D]] in - return routeLeg.steps.map { (routeStep: RouteStep) -> [CLLocationCoordinate2D] in - if let routeShape = routeStep.shape { - return routeShape.coordinates - } else { - return [] - } - } - } - let flatList = nestedList.flatMap { $0.flatMap { $0.compactMap { $0 } } } - return RoutePoints(nestedList: nestedList, flatList: flatList) - } - - func updateRouteLine(routeProgress: RouteProgress) { - updateIntersectionAnnotations(routeProgress: routeProgress) - if let routes { - mapStyleManager.updateRouteAlertsAnnotations( - navigationRoutes: routes, - excludedRouteAlertTypes: excludedRouteAlertTypes, - distanceTraveled: routeProgress.distanceTraveled - ) - } - - if routeLineTracksTraversal, routes != nil { - guard !routeProgress.routeIsComplete else { - mapStyleManager.removeRoutes() - mapStyleManager.removeArrows() - return - } - - updateUpcomingRoutePointIndex(routeProgress: routeProgress) - } - updateArrow(routeProgress: routeProgress) - } - - func updateAlternatives(routeProgress: RouteProgress?) { - guard let routes = routeProgress?.navigationRoutes ?? routes else { return } - show(routes, routeAnnotationKinds: routeAnnotationKinds) - } - - func updateIntersectionAnnotations(routeProgress: RouteProgress?) { - if let routeProgress, showsIntersectionAnnotations { - mapStyleManager.updateIntersectionAnnotations(routeProgress: routeProgress) - } else { - mapStyleManager.removeIntersectionAnnotations() - } - } - - /// Find and cache the index of the upcoming [RouteLineDistancesIndex]. - func updateUpcomingRoutePointIndex(routeProgress: RouteProgress) { - guard let completeRoutePoints = routePoints, - completeRoutePoints.nestedList.indices.contains(routeProgress.legIndex) - else { - routeRemainingDistancesIndex = nil - return - } - let currentLegProgress = routeProgress.currentLegProgress - let currentStepProgress = routeProgress.currentLegProgress.currentStepProgress - let currentLegSteps = completeRoutePoints.nestedList[routeProgress.legIndex] - var allRemainingPoints = 0 - // Find the count of remaining points in the current step. - let lineString = currentStepProgress.step.shape ?? LineString([]) - // If user hasn't arrived at current step. All the coordinates will be included to the remaining points. - if currentStepProgress.distanceTraveled < 0 { - allRemainingPoints += currentLegSteps[currentLegProgress.stepIndex].count - } else if let startIndex = lineString - .indexedCoordinateFromStart(distance: currentStepProgress.distanceTraveled)?.index, - lineString.coordinates.indices.contains(startIndex) - { - allRemainingPoints += lineString.coordinates.suffix(from: startIndex + 1).dropLast().count - } - - // Add to the count of remaining points all of the remaining points on the current leg, after the current step. - if currentLegProgress.stepIndex < currentLegSteps.endIndex { - var count = 0 - for stepIndex in (currentLegProgress.stepIndex + 1).. RouteLineGranularDistances? { - if coordinates.isEmpty { return nil } - var distance = 0.0 - var indexArray = [RouteLineDistancesIndex?](repeating: nil, count: coordinates.count) - for index in stride(from: coordinates.count - 1, to: 0, by: -1) { - let curr = coordinates[index] - let prev = coordinates[index - 1] - distance += curr.projectedDistance(to: prev) - indexArray[index - 1] = RouteLineDistancesIndex(point: prev, distanceRemaining: distance) - } - indexArray[coordinates.count - 1] = RouteLineDistancesIndex( - point: coordinates[coordinates.count - 1], - distanceRemaining: 0.0 - ) - return RouteLineGranularDistances(distance: distance, distanceArray: indexArray.compactMap { $0 }) - } - - func findClosestCoordinateOnCurrentLine( - coordinate: CLLocationCoordinate2D, - granularDistances: RouteLineGranularDistances, - upcomingIndex: Int - ) -> CLLocationCoordinate2D { - guard granularDistances.distanceArray.indices.contains(upcomingIndex) else { return coordinate } - - var coordinates = [CLLocationCoordinate2D]() - - // Takes the passed 10 points and the upcoming point of route to form a sliced polyline for distance - // calculation, incase of the curved shape of route. - for index in max(0, upcomingIndex - 10)...upcomingIndex { - let point = granularDistances.distanceArray[index].point - coordinates.append(point) - } - - let polyline = LineString(coordinates) - - return polyline.closestCoordinate(to: coordinate)?.coordinate ?? coordinate - } - - /// Updates the fractionTraveled along the route line from the origin point to the indicated point. - /// - /// - parameter coordinate: Current position of the user location. - func calculateFractionTraveled(coordinate: CLLocationCoordinate2D) -> Double? { - guard let granularDistances = routeLineGranularDistances, - let index = routeRemainingDistancesIndex, - granularDistances.distanceArray.indices.contains(index) else { return nil } - let traveledIndex = granularDistances.distanceArray[index] - let upcomingPoint = traveledIndex.point - - // Project coordinate onto current line to properly find offset without an issue of back-growing route line. - let coordinate = findClosestCoordinateOnCurrentLine( - coordinate: coordinate, - granularDistances: granularDistances, - upcomingIndex: index + 1 - ) - - // Take the remaining distance from the upcoming point on the route and extends it by the exact position of the - // puck. - let remainingDistance = traveledIndex.distanceRemaining + upcomingPoint.projectedDistance(to: coordinate) - - // Calculate the percentage of the route traveled. - if granularDistances.distance > 0 { - let offset = (1.0 - remainingDistance / granularDistances.distance) - if offset >= 0 { - return offset - } else { - return nil - } - } - return nil - } - - /// Updates the route style layer and its casing style layer to gradually disappear as the user location puck - /// travels along the displayed route. - /// - /// - parameter coordinate: Current position of the user location. - func travelAlongRouteLine(to coordinate: CLLocationCoordinate2D?) { - guard let coordinate, routes != nil else { return } - if let fraction = calculateFractionTraveled(coordinate: coordinate) { - mapStyleManager.setRouteLineOffset(fraction, for: .main) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView.swift deleted file mode 100644 index 222c3d793..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapView.swift +++ /dev/null @@ -1,736 +0,0 @@ -import _MapboxNavigationHelpers -import Combine -import MapboxDirections -import MapboxMaps -import UIKit - -/// `NavigationMapView` is a subclass of `UIView`, which draws `MapView` on its surface and provides -/// convenience functions for adding ``NavigationRoutes`` lines to a map. -@MainActor -open class NavigationMapView: UIView { - private enum Constants { - static let initialMapRect = CGRect(x: 0, y: 0, width: 64, height: 64) - static let initialViewportPadding = UIEdgeInsets(top: 20, left: 20, bottom: 40, right: 20) - } - - /// The `MapView` instance added on top of ``NavigationMapView`` renders navigation-related components. - public let mapView: MapView - - /// ``NavigationCamera``, which allows to control camera states. - public let navigationCamera: NavigationCamera - let mapStyleManager: NavigationMapStyleManager - - /// The object that acts as the navigation delegate of the map view. - public weak var delegate: NavigationMapViewDelegate? - - private var lifetimeSubscriptions: Set = [] - private var viewportDebugView: UIView? - - // Vanishing route line properties - var routePoints: RoutePoints? - var routeLineGranularDistances: RouteLineGranularDistances? - var routeRemainingDistancesIndex: Int? - - private(set) var routes: NavigationRoutes? - - /// The gesture recognizer, that is used to detect taps on waypoints and routes that are currently - /// present on the map. Enabled by default. - public internal(set) var mapViewTapGestureRecognizer: UITapGestureRecognizer! - - /// Initializes ``NavigationMapView`` instance. - /// - Parameters: - /// - location: A publisher that emits current user location. - /// - routeProgress: A publisher that emits route navigation progress. - /// - navigationCameraType: The type of ``NavigationCamera``. Defaults to ``NavigationCameraType/mobile``. - /// which is used for the current instance of ``NavigationMapView``. - /// - heading: A publisher that emits current user heading. Defaults to `nil.` - /// - predictiveCacheManager: An instance of ``PredictiveCacheManager`` used to continuously cache upcoming map - /// tiles. - public init( - location: AnyPublisher, - routeProgress: AnyPublisher, - navigationCameraType: NavigationCameraType = .mobile, - heading: AnyPublisher? = nil, - predictiveCacheManager: PredictiveCacheManager? = nil - ) { - self.mapView = MapView(frame: Constants.initialMapRect).autoresizing() - mapView.location.override( - locationProvider: location.map { [Location(clLocation: $0)] }.eraseToSignal(), - headingProvider: heading?.map { Heading(from: $0) }.eraseToSignal() - ) - - self.mapStyleManager = .init(mapView: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) - self.navigationCamera = NavigationCamera( - mapView, - location: location, - routeProgress: routeProgress, - heading: heading, - navigationCameraType: navigationCameraType - ) - super.init(frame: Constants.initialMapRect) - - mapStyleManager.customizedLayerProvider = customizedLayerProvider - setupMapView() - observeCamera() - enablePredictiveCaching(with: predictiveCacheManager) - subscribeToNavigatonUpdates(routeProgress) - } - - private var currentRouteProgress: RouteProgress? - - // MARK: - Initialization - - private func subscribeToNavigatonUpdates( - _ routeProgressPublisher: AnyPublisher - ) { - routeProgressPublisher - .sink { [weak self] routeProgress in - switch routeProgress { - case nil: - self?.currentRouteProgress = routeProgress - self?.removeRoutes() - case let routeProgress?: - guard let self else { - return - } - let alternativesUpdated = routeProgress.navigationRoutes.alternativeRoutes.map(\.routeId) != routes? - .alternativeRoutes.map(\.routeId) - if routes == nil || routeProgress.routeId != routes?.mainRoute.routeId - || alternativesUpdated - { - show( - routeProgress.navigationRoutes, - routeAnnotationKinds: showsRelativeDurationsOnAlternativeManuever ? - [.relativeDurationsOnAlternativeManuever] : [] - ) - delegate?.navigationMapView( - self, - didAddRedrawActiveGuidanceRoutes: routeProgress.navigationRoutes - ) - } - - currentRouteProgress = routeProgress - updateRouteLine(routeProgress: routeProgress) - } - } - .store(in: &lifetimeSubscriptions) - - routeProgressPublisher - .compactMap { $0 } - .removeDuplicates { $0.legIndex == $1.legIndex } - .sink { [weak self] _ in - self?.updateWaypointsVisiblity() - }.store(in: &lifetimeSubscriptions) - } - - /// `PointAnnotationManager`, which is used to manage addition and removal of a final destination annotation. - /// `PointAnnotationManager` will become valid only after fully loading `MapView` style. - @available( - *, - deprecated, - message: "This property is deprecated and should no longer be used, as the final destination annotation is no longer added to the map. Use 'AnnotationOrchestrator.makePointAnnotationManager()' to create your own annotation manager instead. For more information see the following guide: https://docs.mapbox.com/ios/maps/guides/markers-and-annotations/annotations/#markers" - ) - public private(set) var pointAnnotationManager: PointAnnotationManager? - - private func setupMapView() { - addSubview(mapView) - mapView.pinEdgesToSuperview() - mapView.gestures.delegate = self - mapView.ornaments.options.scaleBar.visibility = .hidden - mapView.preferredFramesPerSecond = 60 - - mapView.location.onPuckRender.sink { [unowned self] data in - travelAlongRouteLine(to: data.location.coordinate) - }.store(in: &lifetimeSubscriptions) - setupGestureRecognizers() - setupUserLocation() - } - - private func observeCamera() { - navigationCamera.cameraStates - .sink { [weak self] cameraState in - guard let self else { return } - delegate?.navigationMapView(self, didChangeCameraState: cameraState) - }.store(in: &lifetimeSubscriptions) - } - - @available(*, unavailable) - override public init(frame: CGRect) { - fatalError("NavigationMapView.init(frame:) is unavailable") - } - - @available(*, unavailable) - public init() { - fatalError("NavigationMapView.init() is unavailable") - } - - @available(*, unavailable) - public required init?(coder: NSCoder) { - fatalError("NavigationMapView.init(coder:) is unavailable") - } - - override open func safeAreaInsetsDidChange() { - super.safeAreaInsetsDidChange() - updateCameraPadding() - } - - // MARK: - Public configuration - - /// The padding applied to the viewport in addition to the safe area. - public var viewportPadding: UIEdgeInsets = Constants.initialViewportPadding { - didSet { updateCameraPadding() } - } - - @_spi(MapboxInternal) public var showsViewportDebugView: Bool = false { - didSet { updateDebugViewportVisibility() } - } - - /// Controls whether to show annotations on intersections, e.g. traffic signals, railroad crossings, yield and stop - /// signs. Defaults to `true`. - public var showsIntersectionAnnotations: Bool = true { - didSet { - updateIntersectionAnnotations(routeProgress: currentRouteProgress) - } - } - - /// Toggles displaying alternative routes. If enabled, view will draw actual alternative route lines on the map. - /// Defaults to `true`. - public var showsAlternatives: Bool = true { - didSet { - updateAlternatives(routeProgress: currentRouteProgress) - } - } - - /// Toggles displaying relative ETA callouts on alternative routes, during active guidance. - /// Defaults to `true`. - public var showsRelativeDurationsOnAlternativeManuever: Bool = true { - didSet { - if showsRelativeDurationsOnAlternativeManuever { - routeAnnotationKinds = [.relativeDurationsOnAlternativeManuever] - } else { - routeAnnotationKinds.removeAll() - } - updateAlternatives(routeProgress: currentRouteProgress) - } - } - - /// Controls whether the main route style layer and its casing disappears as the user location puck travels over it. - /// Defaults to `true`. - /// - /// If `true`, the part of the route that has been traversed will be rendered with full transparency, to give the - /// illusion of a disappearing route. If `false`, the whole route will be shown without traversed part disappearing - /// effect. - public var routeLineTracksTraversal: Bool = true - - /// The maximum distance (in screen points) the user can tap for a selection to be valid when selecting a POI. - public var poiClickableAreaSize: CGFloat = 40 - - /// Controls whether to show restricted portions of a route line. Defaults to true. - public var showsRestrictedAreasOnRoute: Bool = true - - /// Decreases route line opacity based on occlusion from 3D objects. - /// Value `0` disables occlusion, value `1` means fully occluded. Defaults to `0.85`. - public var routeLineOcclusionFactor: Double = 0.85 - - /// Configuration for displaying congestion levels on the route line. - /// Allows to customize the congestion colors and ranges that represent different congestion levels. - public var congestionConfiguration: CongestionConfiguration = .default - - /// Controls whether the traffic should be drawn on the route line or not. Defaults to true. - public var showsTrafficOnRouteLine: Bool = true - - /// Maximum distance (in screen points) the user can tap for a selection to be valid when selecting an alternate - /// route. - public var tapGestureDistanceThreshold: CGFloat = 50 - - /// Controls whether the voice instructions should be drawn on the route line or not. Defaults to `false`. - public var showsVoiceInstructionsOnMap: Bool = false { - didSet { - updateVoiceInstructionsVisiblity() - } - } - - /// Controls whether intermediate waypoints displayed on the route line. Defaults to `true`. - public var showsIntermediateWaypoints: Bool = true { - didSet { - updateWaypointsVisiblity() - } - } - - /// Specifies how the map displays the user’s current location, including the appearance and underlying - /// implementation. - /// - /// By default, this property is set to `PuckType.puck3D(.navigationDefault)` , the bearing source is location - /// course. - public var puckType: PuckType? = .puck3D(.navigationDefault) { - didSet { setupUserLocation() } - } - - /// Specifies if a `Puck` should use `Heading` or `Course` for the bearing. Defaults to `PuckBearing.course`. - public var puckBearing: PuckBearing = .course { - didSet { setupUserLocation() } - } - - /// A custom route line layer position for legacy map styles without slot support. - public var customRouteLineLayerPosition: MapboxMaps.LayerPosition? = nil { - didSet { - mapStyleManager.customRouteLineLayerPosition = customRouteLineLayerPosition - guard let routes else { return } - show(routes, routeAnnotationKinds: routeAnnotationKinds) - } - } - - // MARK: RouteLine Customization - - /// Configures the route line color for the main route. - /// If set, overrides the `.unknown` and `.low` traffic colors. - @objc public dynamic var routeColor: UIColor { - get { - congestionConfiguration.colors.mainRouteColors.unknown - } - set { - congestionConfiguration.colors.mainRouteColors.unknown = newValue - congestionConfiguration.colors.mainRouteColors.low = newValue - } - } - - /// Configures the route line color for alternative routes. - /// If set, overrides the `.unknown` and `.low` traffic colors. - @objc public dynamic var routeAlternateColor: UIColor { - get { - congestionConfiguration.colors.alternativeRouteColors.unknown - } - set { - congestionConfiguration.colors.alternativeRouteColors.unknown = newValue - congestionConfiguration.colors.alternativeRouteColors.low = newValue - } - } - - /// Configures the casing route line color for the main route. - @objc public dynamic var routeCasingColor: UIColor = .defaultRouteCasing - /// Configures the casing route line color for alternative routes. - @objc public dynamic var routeAlternateCasingColor: UIColor = .defaultAlternateLineCasing - /// Configures the color for restricted areas on the route line. - @objc public dynamic var routeRestrictedAreaColor: UIColor = .defaultRouteRestrictedAreaColor - /// Configures the color for the traversed part of the main route. The traversed part is rendered only if the color - /// is not `nil`. - /// Defaults to `nil`. - @objc public dynamic var traversedRouteColor: UIColor? = nil - /// Configures the color of the maneuver arrow. - @objc public dynamic var maneuverArrowColor: UIColor = .defaultManeuverArrow - /// Configures the stroke color of the maneuver arrow. - @objc public dynamic var maneuverArrowStrokeColor: UIColor = .defaultManeuverArrowStroke - - // MARK: Route Annotations Customization - - /// Configures the color of the route annotation for the main route. - @objc public dynamic var routeAnnotationSelectedColor: UIColor = - .defaultSelectedRouteAnnotationColor - /// Configures the color of the route annotation for alternative routes. - @objc public dynamic var routeAnnotationColor: UIColor = .defaultRouteAnnotationColor - /// Configures the text color of the route annotation for the main route. - @objc public dynamic var routeAnnotationSelectedTextColor: UIColor = .defaultSelectedRouteAnnotationTextColor - /// Configures the text color of the route annotation for alternative routes. - @objc public dynamic var routeAnnotationTextColor: UIColor = .defaultRouteAnnotationTextColor - /// Configures the text color of the route annotation for alternative routes when relative duration is greater then - /// the main route. - @objc public dynamic var routeAnnotationMoreTimeTextColor: UIColor = .defaultRouteAnnotationMoreTimeTextColor - /// Configures the text color of the route annotation for alternative routes when relative duration is lesser then - /// the main route. - @objc public dynamic var routeAnnotationLessTimeTextColor: UIColor = .defaultRouteAnnotationLessTimeTextColor - /// Configures the text font of the route annotations. - @objc public dynamic var routeAnnotationTextFont: UIFont = .defaultRouteAnnotationTextFont - /// Configures the waypoint color. - @objc public dynamic var waypointColor: UIColor = .defaultWaypointColor - /// Configures the waypoint stroke color. - @objc public dynamic var waypointStrokeColor: UIColor = .defaultWaypointStrokeColor - - // MARK: - Public methods - - /// Updates the inner navigation camera state. - /// - Parameter navigationCameraState: The navigation camera state. See ``NavigationCameraState`` for the - /// possible values. - public func update(navigationCameraState: NavigationCameraState) { - guard navigationCameraState != navigationCamera.currentCameraState else { return } - navigationCamera.update(cameraState: navigationCameraState) - } - - /// Updates road alerts in the free drive state. In active navigation road alerts are taken automatically from the - /// currently set route. - /// - Parameter roadObjects: An array of road objects to be displayed. - public func updateFreeDriveAlertAnnotations(_ roadObjects: [RoadObjectAhead]) { - mapStyleManager.updateFreeDriveAlertsAnnotations( - roadObjects: roadObjects, - excludedRouteAlertTypes: excludedRouteAlertTypes - ) - } - - // MARK: Customizing and Displaying the Route Line(s) - - /// Visualizes the given routes and it's alternatives, removing any existing from the map. - /// - /// Each route is visualized as a line. Each line is color-coded by traffic congestion, if congestion levels are - /// present. - /// Waypoints along the route are visualized as markers. - /// To only visualize the routes and not the waypoints, or to have more control over the camera, - /// use the ``show(_:routeAnnotationKinds:)`` method. - /// - /// - parameter navigationRoutes: ``NavigationRoutes`` containing routes to visualize. The selected route by - /// `routeIndex` is considered primary, while the remaining routes are displayed as if they are currently deselected - /// or inactive. - /// - parameter routesPresentationStyle: Route lines presentation style. By default the map will be - /// updated to fit all routes. - /// - parameter routeAnnotationKinds: A set of ``RouteAnnotationKind`` that should be displayed. Defaults to - /// ``RouteAnnotationKind/relativeDurationsOnAlternative``. - /// - parameter animated: `true` to asynchronously animate the camera, or `false` to instantaneously - /// zoom and pan the map. Defaults to `false`. - /// - parameter duration: Duration of the animation (in seconds). In case if `animated` parameter - /// is set to `false` this value is ignored. Defaults to `1`. - public func showcase( - _ navigationRoutes: NavigationRoutes, - routesPresentationStyle: RoutesPresentationStyle = .all(), - routeAnnotationKinds: Set = [.relativeDurationsOnAlternative], - animated: Bool = false, - duration: TimeInterval = 1.0 - ) { - show(navigationRoutes, routeAnnotationKinds: routeAnnotationKinds) - mapStyleManager.removeArrows() - - fitCamera( - routes: navigationRoutes, - routesPresentationStyle: routesPresentationStyle, - animated: animated, - duration: duration - ) - } - - private(set) var routeAnnotationKinds: Set = [] - - /// Represents a set of ``RoadAlertType`` values that should be hidden from the map display. - /// By default, this is an empty set, which indicates that all road alerts will be displayed. - /// - /// - Note: If specific `RoadAlertType` values are added to this set, those alerts will be - /// excluded from the map rendering. - public var excludedRouteAlertTypes: RoadAlertType = [] { - didSet { - guard let navigationRoutes = routes else { - return - } - - mapStyleManager.updateRouteAlertsAnnotations( - navigationRoutes: navigationRoutes, - excludedRouteAlertTypes: excludedRouteAlertTypes - ) - } - } - - /// Visualizes the given routes and it's alternatives, removing any existing from the map. - /// - /// Each route is visualized as a line. Each line is color-coded by traffic congestion, if congestion - /// levels are present. To also visualize waypoints and zoom the map to fit, - /// use the ``showcase(_:routesPresentationStyle:routeAnnotationKinds:animated:duration:)`` method. - /// - /// To undo the effects of this method, use ``removeRoutes()`` method. - /// - Parameters: - /// - navigationRoutes: ``NavigationRoutes`` to be displayed on the map. - /// - routeAnnotationKinds: A set of ``RouteAnnotationKind`` that should be displayed. - public func show( - _ navigationRoutes: NavigationRoutes, - routeAnnotationKinds: Set - ) { - removeRoutes() - routes = navigationRoutes - self.routeAnnotationKinds = routeAnnotationKinds - let mainRoute = navigationRoutes.mainRoute.route - if routeLineTracksTraversal { - initPrimaryRoutePoints(route: mainRoute) - } - mapStyleManager.updateRoutes( - navigationRoutes, - config: mapStyleConfig, - featureProvider: customRouteLineFeatureProvider - ) - updateWaypointsVisiblity() - if showsVoiceInstructionsOnMap { - mapStyleManager.updateVoiceInstructions(route: mainRoute) - } - mapStyleManager.updateRouteAnnotations( - navigationRoutes: navigationRoutes, - annotationKinds: routeAnnotationKinds, - config: mapStyleConfig - ) - mapStyleManager.updateRouteAlertsAnnotations( - navigationRoutes: navigationRoutes, - excludedRouteAlertTypes: excludedRouteAlertTypes - ) - } - - /// Removes routes and all visible annotations from the map. - public func removeRoutes() { - routes = nil - routeLineGranularDistances = nil - routeRemainingDistancesIndex = nil - mapStyleManager.removeAllFeatures() - } - - func updateArrow(routeProgress: RouteProgress) { - if routeProgress.currentLegProgress.followOnStep != nil { - mapStyleManager.updateArrows( - route: routeProgress.route, - legIndex: routeProgress.legIndex, - stepIndex: routeProgress.currentLegProgress.stepIndex + 1, - config: mapStyleConfig - ) - } else { - removeArrows() - } - } - - /// Removes the `RouteStep` arrow from the `MapView`. - func removeArrows() { - mapStyleManager.removeArrows() - } - - // MARK: - Debug Viewport - - private func updateDebugViewportVisibility() { - if showsViewportDebugView { - let viewportDebugView = with(UIView(frame: .zero)) { - $0.layer.borderWidth = 1 - $0.layer.borderColor = UIColor.blue.cgColor - $0.backgroundColor = .clear - } - addSubview(viewportDebugView) - self.viewportDebugView = viewportDebugView - viewportDebugView.isUserInteractionEnabled = false - updateViewportDebugView() - } else { - viewportDebugView?.removeFromSuperview() - viewportDebugView = nil - } - } - - private func updateViewportDebugView() { - viewportDebugView?.frame = bounds.inset(by: navigationCamera.viewportPadding) - } - - // MARK: - Camera - - private func updateCameraPadding() { - let padding = viewportPadding - let safeAreaInsets = safeAreaInsets - - navigationCamera.viewportPadding = .init( - top: safeAreaInsets.top + padding.top, - left: safeAreaInsets.left + padding.left, - bottom: safeAreaInsets.bottom + padding.bottom, - right: safeAreaInsets.right + padding.right - ) - updateViewportDebugView() - } - - private func fitCamera( - routes: NavigationRoutes, - routesPresentationStyle: RoutesPresentationStyle, - animated: Bool = false, - duration: TimeInterval - ) { - navigationCamera.stop() - let coordinates: [CLLocationCoordinate2D] - switch routesPresentationStyle { - case .main, .all(shouldFit: false): - coordinates = routes.mainRoute.route.shape?.coordinates ?? [] - case .all(true): - let routes = [routes.mainRoute.route] + routes.alternativeRoutes.map(\.route) - coordinates = MultiLineString(routes.compactMap(\.shape?.coordinates)).coordinates.flatMap { $0 } - } - let initialCameraOptions = CameraOptions( - padding: navigationCamera.viewportPadding, - bearing: 0, - pitch: 0 - ) - do { - let cameraOptions = try mapView.mapboxMap.camera( - for: coordinates, - camera: initialCameraOptions, - coordinatesPadding: nil, - maxZoom: nil, - offset: nil - ) - mapView.camera.ease(to: cameraOptions, duration: animated ? duration : 0.0) - } catch { - Log.error("Failed to fit the camera: \(error.localizedDescription)", category: .navigationUI) - } - } - - // MARK: - Localization - - /// Attempts to localize labels into the preferred language. - /// - /// This method automatically modifies the `SymbolLayer.textField` property of any symbol style - /// layer whose source is the [Mapbox Streets - /// source](https://docs.mapbox.com/vector-tiles/reference/mapbox-streets-v8/#overview). - /// The user can set the system’s preferred language in Settings, General Settings, Language & Region. - /// - /// This method avoids localizing road labels into the preferred language, in an effort - /// to match road signage and the turn banner, which always display road names and exit destinations - /// in the local language. - /// - /// - parameter locale: `Locale` in which the map will attempt to be localized. - /// To use the system’s preferred language, if supported, specify nil. Defaults to `nil`. - public func localizeLabels(locale: Locale? = nil) { - guard let preferredLocale = locale ?? VectorSource - .preferredMapboxStreetsLocale(for: nil) else { return } - mapView.localizeLabels(into: preferredLocale) - } - - private func updateVoiceInstructionsVisiblity() { - if showsVoiceInstructionsOnMap { - mapStyleManager.removeVoiceInstructions() - } else if let routes { - mapStyleManager.updateVoiceInstructions(route: routes.mainRoute.route) - } - } - - private var customRouteLineFeatureProvider: RouteLineFeatureProvider { - .init { [weak self] identifier, sourceIdentifier in - guard let self else { return nil } - return delegate?.navigationMapView( - self, - routeLineLayerWithIdentifier: identifier, - sourceIdentifier: sourceIdentifier - ) - } customRouteCasingLineLayer: { [weak self] identifier, sourceIdentifier in - guard let self else { return nil } - return delegate?.navigationMapView( - self, - routeCasingLineLayerWithIdentifier: identifier, - sourceIdentifier: sourceIdentifier - ) - } customRouteRestrictedAreasLineLayer: { [weak self] identifier, sourceIdentifier in - guard let self else { return nil } - return delegate?.navigationMapView( - self, - routeRestrictedAreasLineLayerWithIdentifier: identifier, - sourceIdentifier: sourceIdentifier - ) - } - } - - private var waypointsFeatureProvider: WaypointFeatureProvider { - .init { [weak self] waypoints, legIndex in - guard let self else { return nil } - return delegate?.navigationMapView(self, shapeFor: waypoints, legIndex: legIndex) - } customCirleLayer: { [weak self] identifier, sourceIdentifier in - guard let self else { return nil } - return delegate?.navigationMapView( - self, - waypointCircleLayerWithIdentifier: identifier, - sourceIdentifier: sourceIdentifier - ) - } customSymbolLayer: { [weak self] identifier, sourceIdentifier in - guard let self else { return nil } - return delegate?.navigationMapView( - self, - waypointSymbolLayerWithIdentifier: identifier, - sourceIdentifier: sourceIdentifier - ) - } - } - - private var customizedLayerProvider: CustomizedLayerProvider { - .init { [weak self] in - guard let self else { return $0 } - return customizedLayer($0) - } - } - - private func customizedLayer(_ layer: T) -> T where T: Layer { - guard let customizedLayer = delegate?.navigationMapView(self, willAdd: layer) else { - return layer - } - guard let customizedLayer = customizedLayer as? T else { - preconditionFailure("The customized layer should have the same layer type as the default layer.") - } - return customizedLayer - } - - private func updateWaypointsVisiblity() { - guard let mainRoute = routes?.mainRoute.route else { - mapStyleManager.removeWaypoints() - return - } - - mapStyleManager.updateWaypoints( - route: mainRoute, - legIndex: currentRouteProgress?.legIndex ?? 0, - config: mapStyleConfig, - featureProvider: waypointsFeatureProvider - ) - } - - // - MARK: User Tracking Features - - private func setupUserLocation() { - mapView.location.options.puckType = puckType ?? .puck2D(.emptyPuck) - mapView.location.options.puckBearing = puckBearing - mapView.location.options.puckBearingEnabled = true - } - - // MARK: Configuring Cache and Tiles Storage - - private var predictiveCacheMapObserver: MapboxMaps.Cancelable? = nil - - /// Setups the Predictive Caching mechanism using provided Options. - /// - /// This will handle all the required manipulations to enable the feature and maintain it during the navigations. - /// Once enabled, it will be present as long as `NavigationMapView` is retained. - /// - /// - parameter options: options, controlling caching parameters like area radius and concurrent downloading - /// threads. - private func enablePredictiveCaching(with predictiveCacheManager: PredictiveCacheManager?) { - predictiveCacheMapObserver?.cancel() - - guard let predictiveCacheManager else { - predictiveCacheMapObserver = nil - return - } - - predictiveCacheManager.updateMapControllers(mapView: mapView) - predictiveCacheMapObserver = mapView.mapboxMap.onStyleLoaded.observe { [ - weak self, - predictiveCacheManager - ] _ in - guard let self else { return } - - predictiveCacheManager.updateMapControllers(mapView: mapView) - } - } - - private var mapStyleConfig: MapStyleConfig { - .init( - routeCasingColor: routeCasingColor, - routeAlternateCasingColor: routeAlternateCasingColor, - routeRestrictedAreaColor: routeRestrictedAreaColor, - traversedRouteColor: traversedRouteColor, - maneuverArrowColor: maneuverArrowColor, - maneuverArrowStrokeColor: maneuverArrowStrokeColor, - routeAnnotationSelectedColor: routeAnnotationSelectedColor, - routeAnnotationColor: routeAnnotationColor, - routeAnnotationSelectedTextColor: routeAnnotationSelectedTextColor, - routeAnnotationTextColor: routeAnnotationTextColor, - routeAnnotationMoreTimeTextColor: routeAnnotationMoreTimeTextColor, - routeAnnotationLessTimeTextColor: routeAnnotationLessTimeTextColor, - routeAnnotationTextFont: routeAnnotationTextFont, - routeLineTracksTraversal: routeLineTracksTraversal, - isRestrictedAreaEnabled: showsRestrictedAreasOnRoute, - showsTrafficOnRouteLine: showsTrafficOnRouteLine, - showsAlternatives: showsAlternatives, - showsIntermediateWaypoints: showsIntermediateWaypoints, - occlusionFactor: .constant(routeLineOcclusionFactor), - congestionConfiguration: congestionConfiguration, - waypointColor: waypointColor, - waypointStrokeColor: waypointStrokeColor - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapViewDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapViewDelegate.swift deleted file mode 100644 index e1cbf6c3e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/NavigationMapViewDelegate.swift +++ /dev/null @@ -1,289 +0,0 @@ -import MapboxDirections -import MapboxMaps -import Turf -import UIKit - -/// The ``NavigationMapViewDelegate`` provides methods for responding to events triggered by the ``NavigationMapView``. -@MainActor -public protocol NavigationMapViewDelegate: AnyObject, UnimplementedLogging { - /// Tells the receiver that the user has selected an alternative route by interacting with the map view. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView``. - /// - alternativeRoute: The selected alternative route. - func navigationMapView(_ navigationMapView: NavigationMapView, didSelect alternativeRoute: AlternativeRoute) - - /// Tells the receiver that the user has tapped on a POI. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView``. - /// - mapPoint: A selected ``MapPoint``. - func navigationMapView(_ navigationMapView: NavigationMapView, userDidTap mapPoint: MapPoint) - - /// Tells the receiver that the user has long tapped on a POI. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView``. - /// - mapPoint: A selected ``MapPoint``. - func navigationMapView(_ navigationMapView: NavigationMapView, userDidLongTap mapPoint: MapPoint) - - /// Tells the receiver that the user has started interacting with the map view, e.g. with panning gesture. - /// - Parameter navigationMapView: The ``NavigationMapView``. - func navigationMapViewUserDidStartInteraction(_ navigationMapView: NavigationMapView) - - /// Tells the receiver that the user has stopped interacting with the map view. - /// - Parameter navigationMapView: The ``NavigationMapView``. - func navigationMapViewUserDidEndInteraction(_ navigationMapView: NavigationMapView) - - /// Tells the receiver that the camera changed its state. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView`` object. - /// - cameraState: A new camera state. - func navigationMapView( - _ navigationMapView: NavigationMapView, - didChangeCameraState cameraState: NavigationCameraState - ) - - /// Tells the receiver that a waypoint was selected. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView``. - /// - waypoint: The waypoint that was selected. - func navigationMapView(_ navigationMapView: NavigationMapView, didSelect waypoint: Waypoint) - - /// Tells the receiver that the final destination `PointAnnotation` was added to the ``NavigationMapView``. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView`` object. - /// - finalDestinationAnnotation: The point annotation that was added to the map view. - /// - pointAnnotationManager: The object that manages the point annotation in the map view. - @available( - *, - deprecated, - message: "This method is deprecated and should no longer be used, as the final destination annotation is no longer added to the map." - ) - func navigationMapView( - _ navigationMapView: NavigationMapView, - didAdd finalDestinationAnnotation: PointAnnotation, - pointAnnotationManager: PointAnnotationManager - ) - - /// Tells the reciever that ``NavigationMapView`` has updated the displayed ``NavigationRoutes`` for the active - /// guidance. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView`` object. - /// - navigationRoutes: New displayed ``NavigationRoutes`` object. - func navigationMapView( - _ navigationMapView: NavigationMapView, - didAddRedrawActiveGuidanceRoutes navigationRoutes: NavigationRoutes - ) - - // MARK: Supplying Waypoint(s) Data - - /// Asks the receiver to return a `CircleLayer` for waypoints, given an identifier and source. - /// This method is invoked any time waypoints are added or shown. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView`` object. - /// - identifier: The `CircleLayer` identifier. - /// - sourceIdentifier: Identifier of the source, which contains the waypoint data that this method would style. - /// - Returns: A `CircleLayer` that the map applies to all waypoints. - func navigationMapView( - _ navigationMapView: NavigationMapView, - waypointCircleLayerWithIdentifier identifier: String, - sourceIdentifier: String - ) -> CircleLayer? - - /// Asks the receiver to return a `SymbolLayer` for intermediate waypoint symbols, given an identifier and source. - /// This method is invoked any time intermediate waypoints are added or shown. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView`` object. - /// - identifier: The `SymbolLayer` identifier. - /// - sourceIdentifier: Identifier of the source, which contains the waypoint data that this method would style. - /// - Returns: A `SymbolLayer` that the map applies to all waypoint symbols. - func navigationMapView( - _ navigationMapView: NavigationMapView, - waypointSymbolLayerWithIdentifier identifier: String, - sourceIdentifier: String - ) -> SymbolLayer? - - /// Asks the receiver to return a `FeatureCollection` that describes the geometry of waypoints. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView`` object. - /// - waypoints: The waypoints to be displayed on the map. - /// - legIndex: The index of the current leg during navigation. - /// - Returns: Optionally, a `FeatureCollection` that defines the shape of the waypoint, or `nil` to use default - /// behavior. - func navigationMapView(_ navigationMapView: NavigationMapView, shapeFor waypoints: [Waypoint], legIndex: Int) - -> FeatureCollection? - - // MARK: Supplying Route Line(s) Data - - /// Asks the receiver to return a `LineLayer` for the route line, given a layer identifier and a source identifier. - /// This method is invoked when the map view loads and any time routes are added. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView`` object. - /// - identifier: The `LineLayer` identifier. - /// - sourceIdentifier: Identifier of the source, which contains the route data that this method would style. - /// - Returns: A `LineLayer` that is applied to the route line. - func navigationMapView( - _ navigationMapView: NavigationMapView, - routeLineLayerWithIdentifier identifier: String, - sourceIdentifier: String - ) -> LineLayer? - - /// Asks the receiver to return a `LineLayer` for the casing layer that surrounds route line, given a layer - /// identifier and a source identifier. This method is invoked when the map view loads and any time routes are - /// added. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView`` object. - /// - identifier: The `LineLayer` identifier. - /// - sourceIdentifier: Identifier of the source, which contains the route data that this method would style. - /// - Returns: A `LineLayer` that is applied as a casing around the route line. - func navigationMapView( - _ navigationMapView: NavigationMapView, - routeCasingLineLayerWithIdentifier identifier: String, - sourceIdentifier: String - ) -> LineLayer? - - /// Asks the receiver to return a `LineLayer` for highlighting restricted areas portions of the route, given a layer - /// identifier and a source identifier. This method is invoked when - /// ``NavigationMapView/showsRestrictedAreasOnRoute`` is enabled, the map view loads and any time routes are added. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView`` object. - /// - identifier: The `LineLayer` identifier. - /// - sourceIdentifier: Identifier of the source, which contains the route data that this method would style. - /// - Returns: A `LineLayer` that is applied as restricted areas on the route line. - func navigationMapView( - _ navigationMapView: NavigationMapView, - routeRestrictedAreasLineLayerWithIdentifier identifier: String, - sourceIdentifier: String - ) -> LineLayer? - - /// Asks the receiver to adjust the default layer which will be added to the map view and return a `Layer`. - /// This method is invoked when the map view loads and any time a layer will be added. - /// - Parameters: - /// - navigationMapView: The ``NavigationMapView`` object. - /// - layer: A default `Layer` generated by the navigationMapView. - /// - Returns: An adjusted `Layer` that will be added to the map view by the SDK. - func navigationMapView(_ navigationMapView: NavigationMapView, willAdd layer: Layer) -> Layer? -} - -extension NavigationMapViewDelegate { - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView( - _ navigationMapView: NavigationMapView, - routeLineLayerWithIdentifier identifier: String, - sourceIdentifier: String - ) -> LineLayer? { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - return nil - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView( - _ navigationMapView: NavigationMapView, - routeCasingLineLayerWithIdentifier identifier: String, - sourceIdentifier: String - ) -> LineLayer? { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - return nil - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView( - _ navigationMapView: NavigationMapView, - routeRestrictedAreasLineLayerWithIdentifier identifier: String, - sourceIdentifier: String - ) -> LineLayer? { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - return nil - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView( - _ navigationMapView: NavigationMapView, - didSelect alternativeRoute: AlternativeRoute - ) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView(_ navigationMapView: NavigationMapView, userDidTap mapPoint: MapPoint) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView(_ navigationMapView: NavigationMapView, userDidLongTap mapPoint: MapPoint) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapViewUserDidStartInteraction(_ navigationMapView: NavigationMapView) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapViewUserDidEndInteraction(_ navigationMapView: NavigationMapView) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView( - _ navigationMapView: NavigationMapView, - didChangeCameraState cameraState: NavigationCameraState - ) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } - - public func navigationMapView(_ navigationMapView: NavigationMapView, didSelect waypoint: Waypoint) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView( - _ navigationMapView: NavigationMapView, - didAdd finalDestinationAnnotation: PointAnnotation, - pointAnnotationManager: PointAnnotationManager - ) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView( - _ navigationMapView: NavigationMapView, - didAddRedrawActiveGuidanceRoutes navigationRoutes: NavigationRoutes - ) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView( - _ navigationMapView: NavigationMapView, - shapeFor waypoints: [Waypoint], - legIndex: Int - ) -> FeatureCollection? { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - return nil - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView( - _ navigationMapView: NavigationMapView, - waypointCircleLayerWithIdentifier identifier: String, - sourceIdentifier: String - ) -> CircleLayer? { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - return nil - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView( - _ navigationMapView: NavigationMapView, - waypointSymbolLayerWithIdentifier identifier: String, - sourceIdentifier: String - ) -> SymbolLayer? { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - return nil - } - - /// ``UnimplementedLogging`` prints a warning to standard output the first time this method is called. - public func navigationMapView(_ navigationMapView: NavigationMapView, willAdd layer: Layer) -> Layer? { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - return nil - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Array.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Array.swift deleted file mode 100644 index 633462993..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Array.swift +++ /dev/null @@ -1,159 +0,0 @@ -import CoreGraphics -import CoreLocation -import Foundation -import MapboxDirections -import Turf - -extension Array { - /// Conditionally remove each element depending on the elements immediately preceding and following it. - /// - /// - parameter shouldBeRemoved: A closure that is called once for each element in reverse order from last to first. - /// The closure accepts the - /// following arguments: the preceding element in the (unreversed) array, the element itself, and the following - /// element in the (unreversed) array. - mutating func removeSeparators(where shouldBeRemoved: (Element?, Element, Element?) throws -> Bool) rethrows { - for (index, element) in enumerated().reversed() { - let precedingElement = lazy.prefix(upTo: index).last - let followingElement = lazy.suffix(from: self.index(after: index)).first - if try shouldBeRemoved(precedingElement, element, followingElement) { - remove(at: index) - } - } - } -} - -extension [RouteStep] { - // Find the longest contiguous series of RouteSteps connected to the first one. - // - // tolerance: -- Maximum distance between the end of one RouteStep and the start of the next to still consider them connected. Defaults to 100 meters - func continuousShape(tolerance: CLLocationDistance = 100) -> LineString? { - guard count > 0 else { return nil } - guard count > 1 else { return self[0].shape } - - let stepShapes = compactMap(\.shape) - let filteredStepShapes = zip(stepShapes, stepShapes.suffix(from: 1)).filter { - guard let maneuverLocation = $1.coordinates.first else { return false } - - return $0.coordinates.last?.distance(to: maneuverLocation) ?? Double.greatestFiniteMagnitude < tolerance - } - - let coordinates = filteredStepShapes.flatMap { firstLine, _ -> [CLLocationCoordinate2D] in - return firstLine.coordinates - } - - return LineString(coordinates) - } -} - -extension Array where Element: NSAttributedString { - /// Returns a new attributed string by concatenating the elements of the array, adding the given separator between - /// each element. - func joined(separator: NSAttributedString = .init()) -> NSAttributedString { - guard let first else { - return NSAttributedString() - } - - let joinedAttributedString = NSMutableAttributedString(attributedString: first) - for element in dropFirst() { - joinedAttributedString.append(separator) - joinedAttributedString.append(element) - } - return joinedAttributedString - } -} - -extension Array where Iterator.Element == CLLocationCoordinate2D { - /// Returns an array of congestion segments by associating the given congestion levels with the coordinates of - /// the respective line segments that they apply to. - /// - /// This method coalesces consecutive line segments that have the same congestion level. - /// - /// For each item in the `CongestionSegment` collection a `CongestionLevel` substitution will take place that - /// has a streets road class contained in the `roadClassesWithOverriddenCongestionLevels` collection. - /// For each of these items the `CongestionLevel` for `.unknown` traffic congestion will be replaced with the - /// `.low` traffic congestion. - /// - /// - parameter congestionLevels: The congestion levels along a leg. There should be one fewer congestion levels - /// than coordinates. - /// - parameter streetsRoadClasses: A collection of streets road classes for each geometry index in - /// `Intersection`. There should be the same amount of `streetsRoadClasses` and `congestions`. - /// - parameter roadClassesWithOverriddenCongestionLevels: Streets road classes for which a `CongestionLevel` - /// substitution should occur. - /// - returns: A list of `CongestionSegment` tuples with coordinate and congestion level. - func combined( - _ congestionLevels: [CongestionLevel], - streetsRoadClasses: [MapboxStreetsRoadClass?]? = nil, - roadClassesWithOverriddenCongestionLevels: Set? = nil - ) - -> [CongestionSegment] { - var segments: [CongestionSegment] = [] - segments.reserveCapacity(congestionLevels.count) - - var index = 0 - for (firstSegment, congestionLevel) in zip(zip(self, suffix(from: 1)), congestionLevels) { - let coordinates = [firstSegment.0, firstSegment.1] - - var overriddenCongestionLevel = congestionLevel - if let streetsRoadClasses, - let roadClassesWithOverriddenCongestionLevels, - streetsRoadClasses.indices.contains(index), - let streetsRoadClass = streetsRoadClasses[index], - congestionLevel == .unknown, - roadClassesWithOverriddenCongestionLevels.contains(streetsRoadClass) - { - overriddenCongestionLevel = .low - } - - if segments.last?.1 == overriddenCongestionLevel { - segments[segments.count - 1].0 += [firstSegment.1] - } else { - segments.append((coordinates, overriddenCongestionLevel)) - } - - index += 1 - } - - return segments - } - - /// Returns an array of road segments by associating road classes of corresponding line segments. - /// - /// Adjacent segments with the same `combiningRoadClasses` will be merged together. - /// - /// - parameter roadClasses: An array of `RoadClasses`along given segment. There should be one fewer congestion - /// levels than coordinates. - /// - parameter combiningRoadClasses: `RoadClasses` which will be joined if they are neighbouring each other. - /// - returns: A list of `RoadClassesSegment` tuples with coordinate and road class. - func combined( - _ roadClasses: [RoadClasses?], - combiningRoadClasses: RoadClasses? = nil - ) -> [RoadClassesSegment] { - var segments: [RoadClassesSegment] = [] - segments.reserveCapacity(roadClasses.count) - - var index = 0 - for (firstSegment, currentRoadClass) in zip(zip(self, suffix(from: 1)), roadClasses) { - let coordinates = [firstSegment.0, firstSegment.1] - var definedRoadClass = currentRoadClass ?? RoadClasses() - definedRoadClass = combiningRoadClasses?.intersection(definedRoadClass) ?? definedRoadClass - - if segments.last?.1 == definedRoadClass { - segments[segments.count - 1].0 += [firstSegment.1] - } else { - segments.append((coordinates, definedRoadClass)) - } - - index += 1 - } - - return segments - } -} - -extension Array { - func chunked(into size: Int) -> [[Element]] { - return stride(from: 0, to: count, by: size).map { - Array(self[$0.. Double { - let distanceArray: [Double] = [ - projectX(longitude) - projectX(coordinate.longitude), - projectY(latitude) - projectY(coordinate.latitude), - ] - return (distanceArray[0] * distanceArray[0] + distanceArray[1] * distanceArray[1]).squareRoot() - } - - private func projectX(_ x: Double) -> Double { - return x / 360.0 + 0.5 - } - - private func projectY(_ y: Double) -> Double { - let sinValue = sin(y * Double.pi / 180) - let newYValue = 0.5 - 0.25 * log((1 + sinValue) / (1 - sinValue)) / Double.pi - if newYValue < 0 { - return 0.0 - } else if newYValue > 1 { - return 1.1 - } else { - return newYValue - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/CongestionSegment.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/CongestionSegment.swift deleted file mode 100644 index 2e92fc485..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/CongestionSegment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import CoreLocation -import MapboxDirections - -/// A tuple that pairs an array of coordinates with the level of -/// traffic congestion along these coordinates. -typealias CongestionSegment = ([CLLocationCoordinate2D], CongestionLevel) diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Cosntants.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Cosntants.swift deleted file mode 100644 index 79c291bb0..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Cosntants.swift +++ /dev/null @@ -1,22 +0,0 @@ -import CoreLocation - -/// A stop dictionary representing the default line widths of the route line by zoom level. -public let RouteLineWidthByZoomLevel: [Double: Double] = [ - 10.0: 8.0, - 13.0: 9.0, - 16.0: 11.0, - 19.0: 22.0, - 22.0: 28.0, -] - -/// Attribute name for the route line that is used for identifying restricted areas along the route. -let RestrictedRoadClassAttribute = "isRestrictedRoad" - -/// Attribute name for the route line that is used for identifying whether a RouteLeg is the current active leg. -let CurrentLegAttribute = "isCurrentLeg" - -/// Attribute name for the route line that is used for identifying different `CongestionLevel` along the route. -let CongestionAttribute = "congestion" - -/// The distance of fading color change between two different congestion level segments in meters. -let GradientCongestionFadingDistance: CLLocationDistance = 30.0 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Expression++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Expression++.swift deleted file mode 100644 index 73052a7d8..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Expression++.swift +++ /dev/null @@ -1,61 +0,0 @@ -import MapboxMaps -import UIKit - -extension MapboxMaps.Expression { - static func routeLineWidthExpression(_ multiplier: Double = 1.0) -> MapboxMaps.Expression { - return Exp(.interpolate) { - Exp(.linear) - Exp(.zoom) - RouteLineWidthByZoomLevel.multiplied(by: multiplier) - } - } - - static func routeCasingLineWidthExpression(_ multiplier: Double = 1.0) -> MapboxMaps.Expression { - routeLineWidthExpression(multiplier * 1.5) - } - - static func routeLineGradientExpression( - _ gradientStops: [Double: UIColor], - lineBaseColor: UIColor, - isSoft: Bool = false - ) -> MapboxMaps.Expression { - if isSoft { - return Exp(.interpolate) { - Exp(.linear) - Exp(.lineProgress) - gradientStops - } - } else { - return Exp(.step) { - Exp(.lineProgress) - lineBaseColor - gradientStops - } - } - } - - static func buildingExtrusionHeightExpression(_ hightProperty: String) -> MapboxMaps.Expression { - return Exp(.interpolate) { - Exp(.linear) - Exp(.zoom) - 13 - 0 - 13.25 - Exp(.get) { - hightProperty - } - } - } -} - -extension [Double: Double] { - /// Returns a copy of the stop dictionary with each value multiplied by the given factor. - public func multiplied(by factor: Double) -> Dictionary { - var newCameraStop: [Double: Double] = [:] - for stop in self { - let newValue = stop.value * factor - newCameraStop[stop.key] = newValue - } - return newCameraStop - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Feature++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Feature++.swift deleted file mode 100644 index 1ee59c5e6..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Feature++.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import Turf - -extension Feature { - var featureIdentifier: Int64? { - guard let featureIdentifier = identifier else { return nil } - - switch featureIdentifier { - case .string(let identifier): - return Int64(identifier) - case .number(let identifier): - return Int64(identifier) - } - } - - enum Property: String { - case poiName = "name" - } - - subscript(property key: Property, languageCode keySuffix: String?) -> JSONValue? { - let jsonValue: JSONValue? = if let keySuffix, let value = properties?["\(key.rawValue)_\(keySuffix)"] { - value - } else { - properties?[key.rawValue].flatMap { $0 } - } - return jsonValue - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/MapboxMap+Async.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/MapboxMap+Async.swift deleted file mode 100644 index ffcf45e4c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/MapboxMap+Async.swift +++ /dev/null @@ -1,54 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxMaps - -extension MapboxMap { - @MainActor - func queryRenderedFeatures( - with point: CGPoint, - options: RenderedQueryOptions? = nil - ) async throws -> [QueriedRenderedFeature] { - let state: CancellableAsyncState = .init() - - return try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { continuation in - let cancellable = queryRenderedFeatures(with: point, options: options) { result in - continuation.resume(with: result) - } - state.activate(with: .init(cancellable)) - } - } onCancel: { - state.cancel() - } - } - - @MainActor - func queryRenderedFeatures( - with rect: CGRect, - options: RenderedQueryOptions? = nil - ) async throws -> [QueriedRenderedFeature] { - let state: CancellableAsyncState = .init() - - return try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { continuation in - let cancellable = queryRenderedFeatures(with: rect, options: options) { result in - continuation.resume(with: result) - } - state.activate(with: .init(cancellable)) - } - } onCancel: { - state.cancel() - } - } -} - -private final class AnyMapboxMapsCancelable: CancellableAsyncStateValue { - private let mapboxMapsCancellable: any MapboxMaps.Cancelable - - init(_ mapboxMapsCancellable: any MapboxMaps.Cancelable) { - self.mapboxMapsCancellable = mapboxMapsCancellable - } - - func cancel() { - mapboxMapsCancellable.cancel() - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift deleted file mode 100644 index 1602e79fa..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation - -extension NavigationMapView { - static let identifier = "com.mapbox.navigation.core" - - @MainActor - enum LayerIdentifier { - static let puck2DLayer: String = "puck" - static let puck3DLayer: String = "puck-model-layer" - static let poiLabelLayer: String = "poi-label" - static let transitLabelLayer: String = "transit-label" - static let airportLabelLayer: String = "airport-label" - - static var clickablePoiLabels: [String] { - [ - LayerIdentifier.poiLabelLayer, - LayerIdentifier.transitLabelLayer, - LayerIdentifier.airportLabelLayer, - ] - } - } - - enum ImageIdentifier { - static let markerImage = "default_marker" - static let midpointMarkerImage = "midpoint_marker" - static let trafficSignal = "traffic_signal" - static let railroadCrossing = "railroad_crossing" - static let yieldSign = "yield_sign" - static let stopSign = "stop_sign" - static let searchAnnotationImage = "search_annotation" - static let selectedSearchAnnotationImage = "search_annotation_selected" - } - - enum ModelKeyIdentifier { - static let modelSouce = "puck-model" - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/PuckConfigurations.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/PuckConfigurations.swift deleted file mode 100644 index 08c4d1301..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/PuckConfigurations.swift +++ /dev/null @@ -1,41 +0,0 @@ -import _MapboxNavigationHelpers -@_spi(Experimental) import MapboxMaps -import UIKit - -extension Puck3DConfiguration { - private static let modelURL = Bundle.mapboxNavigationUXCore.url(forResource: "3DPuck", withExtension: "glb")! - - /// Default 3D user puck configuration - public static let navigationDefault = Puck3DConfiguration( - model: Model(uri: modelURL), - modelScale: .constant([1.5, 1.5, 1.5]), - modelOpacity: .constant(1), - // Turn off shadows as it greatly affect performance due to constant shadow recalculation. - modelCastShadows: .constant(false), - modelReceiveShadows: .constant(false), - modelEmissiveStrength: .constant(0) - ) -} - -extension Puck2DConfiguration { - public static let navigationDefault = Puck2DConfiguration( - topImage: UIColor.clear.image(CGSize(width: 1.0, height: 1.0)), - bearingImage: .init(named: "puck", in: .mapboxNavigationUXCore, compatibleWith: nil), - showsAccuracyRing: false, - opacity: 1 - ) - - static let emptyPuck: Self = { - // Since Mapbox Maps will not provide location data in case if `LocationOptions.puckType` is - // set to nil, we have to draw empty and transparent `UIImage` instead of puck. This is used - // in case when user wants to stop showing location puck or draw a custom one. - let clearImage = UIColor.clear.image(CGSize(width: 1.0, height: 1.0)) - return Puck2DConfiguration( - topImage: clearImage, - bearingImage: clearImage, - shadowImage: clearImage, - scale: nil, - showsAccuracyRing: false - ) - }() -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadAlertType.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadAlertType.swift deleted file mode 100644 index c2739b426..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadAlertType.swift +++ /dev/null @@ -1,121 +0,0 @@ -import MapboxDirections - -/// Represents different types of road alerts that can be displayed on a route. -/// Each alert type corresponds to a specific traffic condition or event that can affect the route. -public struct RoadAlertType: OptionSet { - public let rawValue: UInt - - public init(rawValue: UInt) { - self.rawValue = rawValue - } - - /// Indicates a road alert for an accident on the route. - public static let accident = Self(rawValue: 1 << 0) - - /// Indicates a road alert for traffic congestion on the route. - public static let congestion = Self(rawValue: 1 << 1) - - /// Indicates a road alert for construction along the route. - public static let construction = Self(rawValue: 1 << 2) - - /// Indicates a road alert for a disabled vehicle on the route. - public static let disabledVehicle = Self(rawValue: 1 << 3) - - /// Indicates a road alert for lane restrictions on the route. - public static let laneRestriction = Self(rawValue: 1 << 4) - - /// Indicates a road alert related to mass transit on the route. - public static let massTransit = Self(rawValue: 1 << 5) - - /// Indicates a miscellaneous road alert on the route. - public static let miscellaneous = Self(rawValue: 1 << 6) - - /// Indicates a road alert for news impacting the route. - public static let otherNews = Self(rawValue: 1 << 7) - - /// Indicates a road alert for a planned event impacting the route. - public static let plannedEvent = Self(rawValue: 1 << 8) - - /// Indicates a road alert for a road closure on the route. - public static let roadClosure = Self(rawValue: 1 << 9) - - /// Indicates a road alert for hazardous road conditions on the route. - public static let roadHazard = Self(rawValue: 1 << 10) - - /// Indicates a road alert related to weather conditions affecting the route. - public static let weather = Self(rawValue: 1 << 11) - - /// A collection that includes all possible road alert types. - public static let all: Self = [ - .accident, - .congestion, - .construction, - .disabledVehicle, - .laneRestriction, - .massTransit, - .miscellaneous, - .otherNews, - .plannedEvent, - .roadClosure, - .roadHazard, - .weather, - ] -} - -extension RoadAlertType { - init?(roadObjectKind: RoadObject.Kind) { - switch roadObjectKind { - case .incident(let incident): - guard let roadAlertType = incident?.kind.flatMap(RoadAlertType.init) else { - return nil - } - self = roadAlertType - - case .tollCollection, - .borderCrossing, - .tunnel, - .serviceArea, - .restrictedArea, - .bridge, - .railroadCrossing, - .userDefined, - .ic, - .jct, - .undefined: - return nil - } - } -} - -extension RoadAlertType { - private init?(incident: Incident.Kind) { - switch incident { - case .accident: - self = .accident - case .congestion: - self = .congestion - case .construction: - self = .construction - case .disabledVehicle: - self = .disabledVehicle - case .laneRestriction: - self = .laneRestriction - case .massTransit: - self = .massTransit - case .miscellaneous: - self = .miscellaneous - case .otherNews: - self = .otherNews - case .plannedEvent: - self = .plannedEvent - case .roadClosure: - self = .roadClosure - case .roadHazard: - self = .roadHazard - case .weather: - self = .weather - case .undefined: - return nil - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadClassesSegment.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadClassesSegment.swift deleted file mode 100644 index e2140f9be..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoadClassesSegment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import CoreLocation -import MapboxDirections - -/// A tuple that pairs an array of coordinates with assigned road classes -/// along these coordinates. -typealias RoadClassesSegment = ([CLLocationCoordinate2D], RoadClasses) diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Route.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Route.swift deleted file mode 100644 index e2748034d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/Route.swift +++ /dev/null @@ -1,217 +0,0 @@ -import CoreLocation -import MapboxDirections -import Turf - -extension Route { - /// Returns a polyline extending a given distance in either direction from a given maneuver along the route. - /// - /// The maneuver is identified by a leg index and step index, in case the route doubles back on itself. - /// - /// - parameter legIndex: Zero-based index of the leg containing the maneuver. - /// - parameter stepIndex: Zero-based index of the step containing the maneuver. - /// - parameter distance: Distance by which the resulting polyline extends in either direction from the maneuver. - /// - returns: A polyline whose length is twice `distance` and whose centroid is located at the maneuver. - func polylineAroundManeuver(legIndex: Int, stepIndex: Int, distance: CLLocationDistance) -> LineString { - let precedingLegs = legs.prefix(upTo: legIndex) - let precedingLegCoordinates = precedingLegs.flatMap(\.steps).flatMap { $0.shape?.coordinates ?? [] } - - let leg = legs[legIndex] - let precedingSteps = leg.steps.prefix(upTo: min(stepIndex, leg.steps.count)) - let precedingStepCoordinates = precedingSteps.compactMap { $0.shape?.coordinates }.reduce([], +) - let precedingPolyline = LineString((precedingLegCoordinates + precedingStepCoordinates).reversed()) - - let followingLegs = legs.dropFirst(legIndex + 1) - let followingLegCoordinates = followingLegs.flatMap(\.steps).flatMap { $0.shape?.coordinates ?? [] } - - let followingSteps = leg.steps.dropFirst(stepIndex) - let followingStepCoordinates = followingSteps.compactMap { $0.shape?.coordinates }.reduce([], +) - let followingPolyline = LineString(followingStepCoordinates + followingLegCoordinates) - - // After trimming, reverse the array so that the resulting polyline proceeds in a forward direction throughout. - let trimmedPrecedingCoordinates: [CLLocationCoordinate2D] = if precedingPolyline.coordinates.isEmpty { - [] - } else { - precedingPolyline.trimmed( - from: precedingPolyline.coordinates[0], - distance: distance - )!.coordinates.reversed() - } - // Omit the first coordinate, which is already contained in trimmedPrecedingCoordinates. - if followingPolyline.coordinates.isEmpty { - return LineString(trimmedPrecedingCoordinates) - } else { - return LineString(trimmedPrecedingCoordinates + followingPolyline.trimmed( - from: followingPolyline.coordinates[0], - distance: distance - )!.coordinates.dropFirst()) - } - } - - func restrictedRoadsFeatures() -> [Feature] { - guard shape != nil else { return [] } - - var hasRestriction = false - var features: [Feature] = [] - - for leg in legs { - let legRoadClasses = leg.roadClasses - - // The last coordinate of the preceding step, is shared with the first coordinate of the next step, we don't - // need both. - let legCoordinates: [CLLocationCoordinate2D] = leg.steps.enumerated() - .reduce([]) { allCoordinates, current in - let index = current.offset - let step = current.element - let stepCoordinates = step.shape!.coordinates - - return index == 0 ? stepCoordinates : allCoordinates + stepCoordinates.dropFirst() - } - - let mergedRoadClasses = legCoordinates.combined( - legRoadClasses, - combiningRoadClasses: .restricted - ) - - features.append(contentsOf: mergedRoadClasses.map { (roadClassesSegment: RoadClassesSegment) -> Feature in - var feature = Feature(geometry: .lineString(LineString(roadClassesSegment.0))) - feature.properties = [ - RestrictedRoadClassAttribute: .boolean(roadClassesSegment.1 == .restricted), - ] - - if !hasRestriction, roadClassesSegment.1 == .restricted { - hasRestriction = true - } - - return feature - }) - } - - return hasRestriction ? features : [] - } - - func congestionFeatures( - legIndex: Int? = nil, - rangesConfiguration: CongestionRangesConfiguration, - roadClassesWithOverriddenCongestionLevels: Set? = nil - ) -> [Feature] { - guard let coordinates = shape?.coordinates else { return [] } - var features: [Feature] = [] - - for (index, leg) in legs.enumerated() { - let legFeatures: [Feature] - let currentLegAttribute = (legIndex != nil) ? index == legIndex : true - - // The last coordinate of the preceding step, is shared with the first coordinate of the next step, we don't - // need both. - let legCoordinates: [CLLocationCoordinate2D] = leg.steps.enumerated() - .reduce([]) { allCoordinates, current in - let index = current.offset - let step = current.element - let stepCoordinates = step.shape!.coordinates - return index == 0 ? stepCoordinates : allCoordinates + stepCoordinates.dropFirst() - } - - if let congestionLevels = leg.resolveCongestionLevels(using: rangesConfiguration), - congestionLevels.count < coordinates.count + 2 - { - let mergedCongestionSegments = legCoordinates.combined( - congestionLevels, - streetsRoadClasses: leg.streetsRoadClasses, - roadClassesWithOverriddenCongestionLevels: roadClassesWithOverriddenCongestionLevels - ) - - legFeatures = mergedCongestionSegments.map { (congestionSegment: CongestionSegment) -> Feature in - var feature = Feature(geometry: .lineString(LineString(congestionSegment.0))) - feature.properties = [ - CongestionAttribute: .string(congestionSegment.1.rawValue), - CurrentLegAttribute: .boolean(currentLegAttribute), - ] - - return feature - } - } else { - var feature = Feature(geometry: .lineString(LineString(.init(coordinates: legCoordinates)))) - feature.properties = [ - CurrentLegAttribute: .boolean(currentLegAttribute), - ] - legFeatures = [feature] - } - - features.append(contentsOf: legFeatures) - } - - return features - } - - func leg(containing step: RouteStep) -> RouteLeg? { - return legs.first { $0.steps.contains(step) } - } - - var tollIntersections: [Intersection]? { - let allSteps = legs.flatMap { return $0.steps } - - let allIntersections = allSteps.flatMap { $0.intersections ?? [] } - let intersectionsWithTolls = allIntersections.filter { return $0.tollCollection != nil } - - return intersectionsWithTolls - } - - // returns the list of line segments along the route that fall within given bounding box. Returns nil if there are - // none. Line segments are defined by the route shape coordinates that lay within the bounding box - func shapes(within: Turf.BoundingBox) -> [LineString]? { - guard let coordinates = shape?.coordinates else { return nil } - var lines = [[CLLocationCoordinate2D]]() - var currentLine: [CLLocationCoordinate2D]? - for coordinate in coordinates { - // see if this coordinate lays within the bounds - if within.contains(coordinate) { - // if there is no current line segment then start one - if currentLine == nil { - currentLine = [CLLocationCoordinate2D]() - } - - // append the coordinate to the current line segment - currentLine?.append(coordinate) - } else { - // if there is a current line segment being built then finish it off and reset - if let currentLine { - lines.append(currentLine) - } - currentLine = nil - } - } - - // append any outstanding final segment - if let currentLine { - lines.append(currentLine) - } - currentLine = nil - - // return the segments as LineStrings - return lines.compactMap { coordinateList -> LineString? in - return LineString(coordinateList) - } - } - - /// Returns true if both the legIndex and stepIndex are valid in the route. - func containsStep(at legIndex: Int, stepIndex: Int) -> Bool { - return legs[safe: legIndex]?.steps.indices.contains(stepIndex) ?? false - } - - public var etaDistanceInfo: EtaDistanceInfo? { - .init(distance: distance, travelTime: expectedTravelTime) - } - - public func etaDistanceInfo(forLeg index: Int) -> EtaDistanceInfo? { - guard legs.indices.contains(index) else { return nil } - - let leg = legs[index] - return .init(distance: leg.distance, travelTime: leg.expectedTravelTime) - } -} - -extension RouteStep { - func intersects(_ boundingBox: Turf.BoundingBox) -> Bool { - return shape?.coordinates.contains(where: { boundingBox.contains($0) }) ?? false - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RouteDurationAnnotationTailPosition.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RouteDurationAnnotationTailPosition.swift deleted file mode 100644 index acb4afbdf..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RouteDurationAnnotationTailPosition.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -enum RouteDurationAnnotationTailPosition: Int { - case leading - case trailing -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoutesPresentationStyle.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoutesPresentationStyle.swift deleted file mode 100644 index acdce947f..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/RoutesPresentationStyle.swift +++ /dev/null @@ -1,14 +0,0 @@ -import MapboxMaps - -/// A style that will be used when presenting routes on top of a map view by calling -/// `NavigationMapView.showcase(_:routesPresentationStyle:animated:)`. -public enum RoutesPresentationStyle { - /// Only first route will be presented on a map view. - case main - - /// All routes will be presented on a map view. - /// - /// - parameter shouldFit: If `true` geometry of all routes will be used for camera transition. - /// If `false` geometry of only first route will be used. Defaults to `true`. - case all(shouldFit: Bool = true) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIColor++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIColor++.swift deleted file mode 100644 index 0aa120d0a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIColor++.swift +++ /dev/null @@ -1,40 +0,0 @@ -import MapboxMaps -import UIKit - -@_spi(MapboxInternal) -extension UIColor { - public class var defaultTintColor: UIColor { #colorLiteral(red: 0.1843137255, green: 0.4784313725, blue: 0.7764705882, alpha: 1) } - - public class var defaultRouteCasing: UIColor { .defaultTintColor } - public class var defaultRouteLayer: UIColor { #colorLiteral(red: 0.337254902, green: 0.6588235294, blue: 0.9843137255, alpha: 1) } - public class var defaultAlternateLine: UIColor { #colorLiteral(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) } - public class var defaultAlternateLineCasing: UIColor { #colorLiteral(red: 0.5019607843, green: 0.4980392157, blue: 0.5019607843, alpha: 1) } - public class var defaultManeuverArrowStroke: UIColor { .defaultRouteLayer } - public class var defaultManeuverArrow: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } - - public class var trafficUnknown: UIColor { defaultRouteLayer } - public class var trafficLow: UIColor { defaultRouteLayer } - public class var trafficModerate: UIColor { #colorLiteral(red: 1, green: 0.5843137255, blue: 0, alpha: 1) } - public class var trafficHeavy: UIColor { #colorLiteral(red: 1, green: 0.3019607843, blue: 0.3019607843, alpha: 1) } - public class var trafficSevere: UIColor { #colorLiteral(red: 0.5607843137, green: 0.1411764706, blue: 0.2784313725, alpha: 1) } - - public class var alternativeTrafficUnknown: UIColor { defaultAlternateLine } - public class var alternativeTrafficLow: UIColor { defaultAlternateLine } - public class var alternativeTrafficModerate: UIColor { #colorLiteral(red: 0.75, green: 0.63, blue: 0.53, alpha: 1.0) } - public class var alternativeTrafficHeavy: UIColor { #colorLiteral(red: 0.71, green: 0.51, blue: 0.51, alpha: 1.0) } - public class var alternativeTrafficSevere: UIColor { #colorLiteral(red: 0.71, green: 0.51, blue: 0.51, alpha: 1.0) } - - public class var defaultRouteRestrictedAreaColor: UIColor { #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) } - - public class var defaultRouteAnnotationColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } - public class var defaultSelectedRouteAnnotationColor: UIColor { #colorLiteral(red: 0.1882352941, green: 0.4470588235, blue: 0.9607843137, alpha: 1) } - - public class var defaultRouteAnnotationTextColor: UIColor { #colorLiteral(red: 0.01960784314, green: 0.02745098039, blue: 0.03921568627, alpha: 1) } - public class var defaultSelectedRouteAnnotationTextColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } - - public class var defaultRouteAnnotationMoreTimeTextColor: UIColor { #colorLiteral(red: 0.9215686275, green: 0.1450980392, blue: 0.1647058824, alpha: 1) } - public class var defaultRouteAnnotationLessTimeTextColor: UIColor { #colorLiteral(red: 0.03529411765, green: 0.6666666667, blue: 0.4549019608, alpha: 1) } - - public class var defaultWaypointColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } - public class var defaultWaypointStrokeColor: UIColor { #colorLiteral(red: 0.137254902, green: 0.1490196078, blue: 0.1764705882, alpha: 1) } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIFont.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIFont.swift deleted file mode 100644 index 15677385a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIFont.swift +++ /dev/null @@ -1,8 +0,0 @@ -import UIKit - -@_spi(MapboxInternal) -extension UIFont { - public class var defaultRouteAnnotationTextFont: UIFont { - .systemFont(ofSize: 18) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIImage++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIImage++.swift deleted file mode 100644 index 744f0c016..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/UIImage++.swift +++ /dev/null @@ -1,47 +0,0 @@ -import UIKit - -extension UIImage { - static let midpointMarkerImage = UIImage( - named: "midpoint_marker", - in: .mapboxNavigationUXCore, - compatibleWith: nil - )! - - convenience init?(color: UIColor, size: CGSize = CGSize(width: 1.0, height: 1.0)) { - let rect = CGRect(origin: .zero, size: size) - UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0) - color.setFill() - UIRectFill(rect) - - let image = UIGraphicsGetImageFromCurrentImageContext() - defer { UIGraphicsEndImageContext() } - - guard let cgImage = image?.cgImage else { return nil } - self.init(cgImage: cgImage) - } - - func tint(_ tintColor: UIColor) -> UIImage { - let imageSize = size - let imageScale = scale - let contextBounds = CGRect(origin: .zero, size: imageSize) - - UIGraphicsBeginImageContextWithOptions(imageSize, false, imageScale) - - defer { UIGraphicsEndImageContext() } - - UIColor.black.setFill() - UIRectFill(contextBounds) - draw(at: .zero) - - guard let imageOverBlack = UIGraphicsGetImageFromCurrentImageContext() else { return self } - tintColor.setFill() - UIRectFill(contextBounds) - - imageOverBlack.draw(at: .zero, blendMode: .multiply, alpha: 1) - draw(at: .zero, blendMode: .destinationIn, alpha: 1) - - guard let finalImage = UIGraphicsGetImageFromCurrentImageContext() else { return self } - - return finalImage - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/VectorSource++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/VectorSource++.swift deleted file mode 100644 index c78595738..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Other/VectorSource++.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation -import MapboxMaps - -extension VectorSource { - /// A dictionary associating known tile set identifiers with identifiers of source layers that contain road names. - static let roadLabelLayerIdentifiersByTileSetIdentifier = [ - "mapbox.mapbox-streets-v8": "road", - "mapbox.mapbox-streets-v7": "road_label", - ] - - /// Method, which returns a boolean value indicating whether the tile source is a supported version of the Mapbox - /// Streets source. - static func isMapboxStreets(_ identifiers: [String]) -> Bool { - return identifiers.contains("mapbox.mapbox-streets-v8") || identifiers.contains("mapbox.mapbox-streets-v7") - } - - /// An array of locales for which Mapbox Streets source v8 has a - /// [dedicated name - /// field](https://docs.mapbox.com/vector-tiles/reference/mapbox-streets-v8/#name-text--name_lang-code-text). - static let mapboxStreetsLocales = [ - "ar", - "de", - "en", - "es", - "fr", - "it", - "ja", - "ko", - "pt", - "ru", - "vi", - "zh-Hans", - "zh-Hant", - ].map(Locale.init(identifier:)) - - /// Returns the BCP 47 language tag supported by Mapbox Streets source v8 that is most preferred according to the - /// given preferences. - static func preferredMapboxStreetsLocalization(among preferences: [String]) -> String? { - let preferredLocales = preferences.map(Locale.init(identifier:)) - let acceptsEnglish = preferredLocales.contains { $0.languageCode == "en" } - var availableLocales = mapboxStreetsLocales - if !acceptsEnglish { - availableLocales.removeAll { $0.languageCode == "en" } - } - - let mostSpecificLanguage = Bundle.preferredLocalizations( - from: availableLocales.map(\.identifier), - forPreferences: preferences - ) - .max { $0.count > $1.count } - - // `Bundle.preferredLocalizations(from:forPreferences:)` is just returning the first localization it could find. - if let mostSpecificLanguage, - !preferredLocales - .contains(where: { $0.languageCode == Locale(identifier: mostSpecificLanguage).languageCode }) - { - return nil - } - - return mostSpecificLanguage - } - - /// Returns the locale supported by Mapbox Streets source v8 that is most preferred for the given locale. - /// - /// - parameter locale: The locale to match. To use the system’s preferred language, if supported, specify `nil`. - /// To use the local language, specify a locale with the identifier `mul`. - static func preferredMapboxStreetsLocale(for locale: Locale?) -> Locale? { - guard locale?.languageCode != "mul" else { - // FIXME: Unlocalization not yet implemented: https://github.com/mapbox/mapbox-maps-ios/issues/653 - return nil - } - - let preferences: [String] = if let locale { - [locale.identifier] - } else { - Locale.preferredLanguages - } - - guard let preferredLocalization = VectorSource.preferredMapboxStreetsLocalization(among: preferences) else { - return nil - } - return Locale(identifier: preferredLocalization) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift deleted file mode 100644 index 172b9b1b0..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation - -extension AlternativeRoute { - /// Returns offset of the alternative route where it deviates from the main route. - func deviationOffset() -> Double { - guard let coordinates = route.shape?.coordinates, - !coordinates.isEmpty - else { - return 0 - } - - let splitGeometryIndex = alternativeRouteIntersectionIndices.routeGeometryIndex - - var totalDistance = 0.0 - var pointDistance: Double? = nil - for index in stride(from: coordinates.count - 1, to: 0, by: -1) { - let currCoordinate = coordinates[index] - let prevCoordinate = coordinates[index - 1] - totalDistance += currCoordinate.projectedDistance(to: prevCoordinate) - - if index == splitGeometryIndex + 1 { - pointDistance = totalDistance - } - } - guard let pointDistance, totalDistance != 0 else { return 0 } - - return (totalDistance - pointDistance) / totalDistance - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionColorsConfiguration.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionColorsConfiguration.swift deleted file mode 100644 index d7a2ac02e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionColorsConfiguration.swift +++ /dev/null @@ -1,76 +0,0 @@ -import UIKit - -/// Configuration settings for congestion colors for the main and alternative routes. -public struct CongestionColorsConfiguration: Equatable, Sendable { - /// Color schema for the main route. - public var mainRouteColors: Colors - /// Color schema for the alternative route. - public var alternativeRouteColors: Colors - - /// Default colors configuration. - public static let `default` = CongestionColorsConfiguration( - mainRouteColors: .defaultMainRouteColors, - alternativeRouteColors: .defaultAlternativeRouteColors - ) - - /// Creates a new ``CongestionColorsConfiguration`` instance. - /// - Parameters: - /// - mainRouteColors: Color schema for the main route. - /// - alternativeRouteColors: Color schema for the alternative route. - public init( - mainRouteColors: CongestionColorsConfiguration.Colors, - alternativeRouteColors: CongestionColorsConfiguration.Colors - ) { - self.mainRouteColors = mainRouteColors - self.alternativeRouteColors = alternativeRouteColors - } -} - -extension CongestionColorsConfiguration { - /// Set of colors for different congestion levels. - public struct Colors: Equatable, Sendable { - /// Assigned color for `low` traffic. - public var low: UIColor - /// Assigned color for `moderate` traffic. - public var moderate: UIColor - /// Assigned color for `heavy` traffic. - public var heavy: UIColor - /// Assigned color for `severe` traffic. - public var severe: UIColor - /// Assigned color for `unknown` traffic. - public var unknown: UIColor - - /// Default color scheme for the main route. - public static let defaultMainRouteColors = Colors( - low: .trafficLow, - moderate: .trafficModerate, - heavy: .trafficHeavy, - severe: .trafficSevere, - unknown: .trafficUnknown - ) - - /// Default color scheme for the alternative route. - public static let defaultAlternativeRouteColors = Colors( - low: .alternativeTrafficLow, - moderate: .alternativeTrafficModerate, - heavy: .alternativeTrafficHeavy, - severe: .alternativeTrafficSevere, - unknown: .alternativeTrafficUnknown - ) - - /// Creates a new ``CongestionColorsConfiguration`` instance. - /// - Parameters: - /// - low: Assigned color for `low` traffic. - /// - moderate: Assigned color for `moderate` traffic. - /// - heavy: Assigned color for `heavy` traffic. - /// - severe: Assigned color for `severe` traffic. - /// - unknown: Assigned color for `unknown` traffic. - public init(low: UIColor, moderate: UIColor, heavy: UIColor, severe: UIColor, unknown: UIColor) { - self.low = low - self.moderate = moderate - self.heavy = heavy - self.severe = severe - self.unknown = unknown - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionConfiguration.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionConfiguration.swift deleted file mode 100644 index 0f2bd5475..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/Congestion/CongestionConfiguration.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -/// Configuration for displaying roads congestion. -public struct CongestionConfiguration: Equatable, Sendable { - /// Colors schema used. - public var colors: CongestionColorsConfiguration - /// Range configuration for congestion. - public var ranges: CongestionRangesConfiguration - - /// Default configuration. - public static let `default` = CongestionConfiguration( - colors: .default, - ranges: .default - ) - - /// Creates a new ``CongestionConfiguration`` instance. - /// - Parameters: - /// - colors: Colors schema used. - /// - ranges: Range configuration for congestion. - public init(colors: CongestionColorsConfiguration, ranges: CongestionRangesConfiguration) { - self.colors = colors - self.ranges = ranges - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/FeatureIds.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/FeatureIds.swift deleted file mode 100644 index 994ef0f96..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/FeatureIds.swift +++ /dev/null @@ -1,169 +0,0 @@ - -enum FeatureIds { - private static let globalPrefix: String = "com.mapbox.navigation" - - struct RouteLine: Hashable, Sendable { - private static let prefix: String = "\(globalPrefix).route_line" - - static var main: Self { - .init(routeId: "\(prefix).main") - } - - static func alternative(idx: Int) -> Self { - .init(routeId: "\(prefix).alternative_\(idx)") - } - - let source: String - let main: String - let casing: String - - let restrictedArea: String - let restrictedAreaSource: String - let traversedRoute: String - - init(routeId: String) { - self.source = routeId - self.main = routeId - self.casing = "\(routeId).casing" - self.restrictedArea = "\(routeId).restricted_area" - self.restrictedAreaSource = "\(routeId).restricted_area" - self.traversedRoute = "\(routeId).traversed_route" - } - } - - struct ManeuverArrow { - private static let prefix: String = "\(globalPrefix).arrow" - - let id: String - let symbolId: String - let arrow: String - let arrowStroke: String - let arrowSymbol: String - let arrowSymbolCasing: String - let arrowSource: String - let arrowSymbolSource: String - let triangleTipImage: String - - init(arrowId: String) { - let id = "\(Self.prefix).\(arrowId)" - self.id = id - self.symbolId = "\(id).symbol" - self.arrow = "\(id)" - self.arrowStroke = "\(id).stroke" - self.arrowSymbol = "\(id).symbol" - self.arrowSymbolCasing = "\(id).symbol.casing" - self.arrowSource = "\(id).source" - self.arrowSymbolSource = "\(id).symbol_source" - self.triangleTipImage = "\(id).triangle_tip_image" - } - - static func nextArrow() -> Self { - .init(arrowId: "next") - } - } - - struct VoiceInstruction { - private static let prefix: String = "\(globalPrefix).voice_instruction" - - let featureId: String - let source: String - let layer: String - let circleLayer: String - - init() { - let id = "\(Self.prefix)" - self.featureId = id - self.source = "\(id).source" - self.layer = "\(id).layer" - self.circleLayer = "\(id).layer.circle" - } - - static var currentRoute: Self { - .init() - } - } - - struct IntersectionAnnotation { - private static let prefix: String = "\(globalPrefix).intersection_annotations" - - let featureId: String - let source: String - let layer: String - - let yieldSignImage: String - let stopSignImage: String - let railroadCrossingImage: String - let trafficSignalImage: String - - init() { - let id = "\(Self.prefix)" - self.featureId = id - self.source = "\(id).source" - self.layer = "\(id).layer" - self.yieldSignImage = "\(id).yield_sign" - self.stopSignImage = "\(id).stop_sign" - self.railroadCrossingImage = "\(id).railroad_crossing" - self.trafficSignalImage = "\(id).traffic_signal" - } - - static var currentRoute: Self { - .init() - } - } - - struct RouteAlertAnnotation { - private static let prefix: String = "\(globalPrefix).route_alert_annotations" - - let featureId: String - let source: String - let layer: String - - init() { - let id = "\(Self.prefix)" - self.featureId = id - self.source = "\(id).source" - self.layer = "\(id).layer" - } - - static var `default`: Self { - .init() - } - } - - struct RouteWaypoints { - private static let prefix: String = "\(globalPrefix)_waypoint" - - let featureId: String - let innerCircle: String - let markerIcon: String - let source: String - - init() { - self.featureId = "\(Self.prefix).route-waypoints" - self.innerCircle = "\(Self.prefix).innerCircleLayer" - self.markerIcon = "\(Self.prefix).symbolLayer" - self.source = "\(Self.prefix).source" - } - - static var `default`: Self { - .init() - } - } - - struct RouteAnnotation: Hashable, Sendable { - private static let prefix: String = "\(globalPrefix).route_line.annotation" - let layerId: String - - static var main: Self { - .init(annotationId: "\(prefix).main") - } - - static func alternative(index: Int) -> Self { - .init(annotationId: "\(prefix).alternative_\(index)") - } - - init(annotationId: String) { - self.layerId = annotationId - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift deleted file mode 100644 index ae7c32362..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift +++ /dev/null @@ -1,133 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxDirections -import MapboxMaps -import enum SwiftUI.ColorScheme - -extension RouteProgress { - func intersectionAnnotationsMapFeatures( - ids: FeatureIds.IntersectionAnnotation, - customizedLayerProvider: CustomizedLayerProvider - ) -> [any MapFeature] { - guard !routeIsComplete else { - return [] - } - - var featureCollection = FeatureCollection(features: []) - - let stepProgress = currentLegProgress.currentStepProgress - let intersectionIndex = stepProgress.intersectionIndex - let intersections = stepProgress.intersectionsIncludingUpcomingManeuverIntersection ?? [] - let stepIntersections = Array(intersections.dropFirst(intersectionIndex)) - - for intersection in stepIntersections { - if let feature = intersectionFeature(from: intersection, ids: ids) { - featureCollection.features.append(feature) - } - } - - let layers: [any Layer] = [ - with(SymbolLayer(id: ids.layer, source: ids.source)) { - $0.iconAllowOverlap = .constant(false) - $0.iconImage = .expression(Exp(.get) { - "imageName" - }) - }, - ] - return [ - GeoJsonMapFeature( - id: ids.featureId, - sources: [ - .init( - id: ids.source, - geoJson: .featureCollection(featureCollection) - ), - ], - customizeSource: { _, _ in }, - layers: layers.map { customizedLayerProvider.customizedLayer($0) }, - onBeforeAdd: { mapView in - Self.upsertIntersectionSymbolImages( - map: mapView.mapboxMap, - ids: ids - ) - }, - onUpdate: { mapView in - Self.upsertIntersectionSymbolImages( - map: mapView.mapboxMap, - ids: ids - ) - }, - onAfterRemove: { mapView in - do { - try Self.removeIntersectionSymbolImages( - map: mapView.mapboxMap, - ids: ids - ) - } catch { - Log.error( - "Failed to remove intersection annotation images with error \(error)", - category: .navigationUI - ) - } - } - ), - ] - } - - private func intersectionFeature( - from intersection: Intersection, - ids: FeatureIds.IntersectionAnnotation - ) -> Feature? { - var properties: JSONObject? - if intersection.yieldSign == true { - properties = ["imageName": .string(ids.yieldSignImage)] - } - if intersection.stopSign == true { - properties = ["imageName": .string(ids.stopSignImage)] - } - if intersection.railroadCrossing == true { - properties = ["imageName": .string(ids.railroadCrossingImage)] - } - if intersection.trafficSignal == true { - properties = ["imageName": .string(ids.trafficSignalImage)] - } - - guard let properties else { return nil } - - var feature = Feature(geometry: .point(Point(intersection.location))) - feature.properties = properties - return feature - } - - private static func upsertIntersectionSymbolImages( - map: MapboxMap, - ids: FeatureIds.IntersectionAnnotation - ) { - for (imageName, imageIdentifier) in imageNameToMapIdentifier(ids: ids) { - if let image = Bundle.module.image(named: imageName) { - map.provisionImage(id: imageIdentifier) { style in - try style.addImage(image, id: imageIdentifier) - } - } - } - } - - private static func removeIntersectionSymbolImages( - map: MapboxMap, - ids: FeatureIds.IntersectionAnnotation - ) throws { - for (_, imageIdentifier) in imageNameToMapIdentifier(ids: ids) { - try map.removeImage(withId: imageIdentifier) - } - } - - private static func imageNameToMapIdentifier( - ids: FeatureIds.IntersectionAnnotation - ) -> [String: String] { - return [ - "TrafficSignal": ids.trafficSignalImage, - "RailroadCrossing": ids.railroadCrossingImage, - "YieldSign": ids.yieldSignImage, - "StopSign": ids.stopSignImage, - ] - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift deleted file mode 100644 index 65a693dc0..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift +++ /dev/null @@ -1,131 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import MapboxDirections -@_spi(Experimental) import MapboxMaps - -extension Route { - func maneuverArrowMapFeatures( - ids: FeatureIds.ManeuverArrow, - cameraZoom: CGFloat, - legIndex: Int, stepIndex: Int, - config: MapStyleConfig, - customizedLayerProvider: CustomizedLayerProvider - ) -> [any MapFeature] { - guard containsStep(at: legIndex, stepIndex: stepIndex) - else { return [] } - - let triangleImage = Bundle.module.image(named: "triangle")!.withRenderingMode(.alwaysTemplate) - - var mapFeatures: [any MapFeature] = [] - - let step = legs[legIndex].steps[stepIndex] - let maneuverCoordinate = step.maneuverLocation - guard step.maneuverType != .arrive else { return [] } - - let metersPerPoint = Projection.metersPerPoint( - for: maneuverCoordinate.latitude, - zoom: cameraZoom - ) - - // TODO: Implement ability to change `shaftLength` depending on zoom level. - let shaftLength = max(min(50 * metersPerPoint, 50), 30) - let shaftPolyline = polylineAroundManeuver(legIndex: legIndex, stepIndex: stepIndex, distance: shaftLength) - - if shaftPolyline.coordinates.count > 1 { - let minimumZoomLevel = 14.5 - let shaftStrokeCoordinates = shaftPolyline.coordinates - let shaftDirection = shaftStrokeCoordinates[shaftStrokeCoordinates.count - 2] - .direction(to: shaftStrokeCoordinates.last!) - let point = Point(shaftStrokeCoordinates.last!) - - let layers: [any Layer] = [ - with(LineLayer(id: ids.arrow, source: ids.arrowSource)) { - $0.minZoom = Double(minimumZoomLevel) - $0.lineCap = .constant(.butt) - $0.lineJoin = .constant(.round) - $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.70)) - $0.lineColor = .constant(.init(config.maneuverArrowColor)) - $0.lineEmissiveStrength = .constant(1) - }, - with(LineLayer(id: ids.arrowStroke, source: ids.arrowSource)) { - $0.minZoom = Double(minimumZoomLevel) - $0.lineCap = .constant(.butt) - $0.lineJoin = .constant(.round) - $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.80)) - $0.lineColor = .constant(.init(config.maneuverArrowStrokeColor)) - $0.lineEmissiveStrength = .constant(1) - }, - with(SymbolLayer(id: ids.arrowSymbol, source: ids.arrowSymbolSource)) { - $0.minZoom = Double(minimumZoomLevel) - $0.iconImage = .constant(.name(ids.triangleTipImage)) - $0.iconColor = .constant(.init(config.maneuverArrowColor)) - $0.iconRotationAlignment = .constant(.map) - $0.iconRotate = .constant(.init(shaftDirection)) - $0.iconSize = .expression(Expression.routeLineWidthExpression(0.12)) - $0.iconAllowOverlap = .constant(true) - $0.iconEmissiveStrength = .constant(1) - }, - with(SymbolLayer(id: ids.arrowSymbolCasing, source: ids.arrowSymbolSource)) { - $0.minZoom = Double(minimumZoomLevel) - $0.iconImage = .constant(.name(ids.triangleTipImage)) - $0.iconColor = .constant(.init(config.maneuverArrowStrokeColor)) - $0.iconRotationAlignment = .constant(.map) - $0.iconRotate = .constant(.init(shaftDirection)) - $0.iconSize = .expression(Expression.routeLineWidthExpression(0.14)) - $0.iconAllowOverlap = .constant(true) - }, - ] - - mapFeatures.append( - GeoJsonMapFeature( - id: ids.id, - sources: [ - .init( - id: ids.arrowSource, - geoJson: .feature(Feature(geometry: .lineString(shaftPolyline))) - ), - .init( - id: ids.arrowSymbolSource, - geoJson: .feature(Feature(geometry: .point(point))) - ), - ], - customizeSource: { source, _ in - source.tolerance = 0.375 - }, - layers: layers.map { customizedLayerProvider.customizedLayer($0) }, - onBeforeAdd: { mapView in - mapView.mapboxMap.provisionImage(id: ids.triangleTipImage) { - try $0.addImage( - triangleImage, - id: ids.triangleTipImage, - sdf: true, - stretchX: [], - stretchY: [] - ) - } - }, - onUpdate: { mapView in - try with(mapView.mapboxMap) { - try $0.setLayerProperty( - for: ids.arrowSymbol, - property: "icon-rotate", - value: shaftDirection - ) - try $0.setLayerProperty( - for: ids.arrowSymbolCasing, - property: "icon-rotate", - value: shaftDirection - ) - } - }, - onAfterRemove: { mapView in - if mapView.mapboxMap.imageExists(withId: ids.triangleTipImage) { - try? mapView.mapboxMap.removeImage(withId: ids.triangleTipImage) - } - } - ) - ) - } - return mapFeatures - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift deleted file mode 100644 index 40063dbd1..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift +++ /dev/null @@ -1,285 +0,0 @@ -import MapboxMaps -import UIKit - -final class ETAView: UIView { - private let label = { - let label = UILabel() - label.textAlignment = .left - return label - }() - - private var tail = UIView() - private let backgroundShape = CAShapeLayer() - private let mapStyleConfig: MapStyleConfig - - let textColor: UIColor - let baloonColor: UIColor - var padding = UIEdgeInsets(allEdges: 10) - var tailSize = 8.0 - var cornerRadius = 8.0 - - var text: String { - didSet { update() } - } - - var anchor: ViewAnnotationAnchor? { - didSet { setNeedsLayout() } - } - - convenience init( - eta: TimeInterval, - isSelected: Bool, - tollsHint: Bool?, - mapStyleConfig: MapStyleConfig - ) { - let viewLabel = DateComponentsFormatter.travelTimeString(eta, signed: false) - - let textColor: UIColor - let baloonColor: UIColor - if isSelected { - textColor = mapStyleConfig.routeAnnotationSelectedTextColor - baloonColor = mapStyleConfig.routeAnnotationSelectedColor - } else { - textColor = mapStyleConfig.routeAnnotationTextColor - baloonColor = mapStyleConfig.routeAnnotationColor - } - - self.init( - text: viewLabel, - tollsHint: tollsHint, - mapStyleConfig: mapStyleConfig, - textColor: textColor, - baloonColor: baloonColor - ) - } - - convenience init( - travelTimeDelta: TimeInterval, - tollsHint: Bool?, - mapStyleConfig: MapStyleConfig - ) { - let textColor: UIColor - let timeDelta: String - if abs(travelTimeDelta) >= 180 { - textColor = if travelTimeDelta > 0 { - mapStyleConfig.routeAnnotationMoreTimeTextColor - } else { - mapStyleConfig.routeAnnotationLessTimeTextColor - } - timeDelta = DateComponentsFormatter.travelTimeString( - travelTimeDelta, - signed: true - ) - } else { - textColor = mapStyleConfig.routeAnnotationTextColor - timeDelta = "SAME_TIME".localizedString( - value: "Similar ETA", - comment: "Alternatives selection note about equal travel time." - ) - } - - self.init( - text: timeDelta, - tollsHint: tollsHint, - mapStyleConfig: mapStyleConfig, - textColor: textColor, - baloonColor: mapStyleConfig.routeAnnotationColor - ) - } - - init( - text: String, - tollsHint: Bool?, - mapStyleConfig: MapStyleConfig, - textColor: UIColor = .darkText, - baloonColor: UIColor = .white - ) { - var viewLabel = text - switch tollsHint { - case .none: - label.numberOfLines = 1 - case .some(true): - label.numberOfLines = 2 - viewLabel += "\n" + "ROUTE_HAS_TOLLS".localizedString( - value: "Tolls", - comment: "Route callout label, indicating there are tolls on the route.") - if let symbol = Locale.current.currencySymbol { - viewLabel += " " + symbol - } - case .some(false): - label.numberOfLines = 2 - viewLabel += "\n" + "ROUTE_HAS_NO_TOLLS".localizedString( - value: "No Tolls", - comment: "Route callout label, indicating there are no tolls on the route.") - } - - self.text = viewLabel - self.textColor = textColor - self.baloonColor = baloonColor - self.mapStyleConfig = mapStyleConfig - super.init(frame: .zero) - layer.addSublayer(backgroundShape) - backgroundShape.shadowRadius = 1.4 - backgroundShape.shadowOffset = CGSize(width: 0, height: 0.7) - backgroundShape.shadowColor = UIColor.black.cgColor - backgroundShape.shadowOpacity = 0.3 - - addSubview(label) - - update() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private var attributedText: NSAttributedString { - let text = NSMutableAttributedString( - attributedString: .labelText( - text, - font: mapStyleConfig.routeAnnotationTextFont, - color: textColor - ) - ) - return text - } - - private func update() { - backgroundShape.fillColor = baloonColor.cgColor - label.attributedText = attributedText - } - - struct Layout { - var label: CGRect - var bubble: CGRect - var size: CGSize - - init(availableSize: CGSize, text: NSAttributedString, tailSize: CGFloat, padding: UIEdgeInsets) { - let tailPadding = UIEdgeInsets(allEdges: tailSize) - - let textPadding = padding + tailPadding + UIEdgeInsets.zero - let textAvailableSize = availableSize - textPadding - let textSize = text.boundingRect( - with: textAvailableSize, - options: .usesLineFragmentOrigin, context: nil - ).size.roundedUp() - self.label = CGRect(padding: textPadding, size: textSize) - self.bubble = CGRect(padding: tailPadding, size: textSize + textPadding - tailPadding) - self.size = bubble.size + tailPadding - } - } - - override func sizeThatFits(_ size: CGSize) -> CGSize { - Layout(availableSize: size, text: attributedText, tailSize: tailSize, padding: padding).size - } - - override func layoutSubviews() { - super.layoutSubviews() - - let layout = Layout(availableSize: bounds.size, text: attributedText, tailSize: tailSize, padding: padding) - label.frame = layout.label - - let calloutPath = UIBezierPath.calloutPath( - size: bounds.size, - tailSize: tailSize, - cornerRadius: cornerRadius, - anchor: anchor ?? .center - ) - backgroundShape.path = calloutPath.cgPath - backgroundShape.frame = bounds - } -} - -extension UIEdgeInsets { - fileprivate init(allEdges value: CGFloat) { - self.init(top: value, left: value, bottom: value, right: value) - } -} - -extension NSAttributedString { - fileprivate static func labelText(_ string: String, font: UIFont, color: UIColor) -> NSAttributedString { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .left - let attributes = [ - NSAttributedString.Key.paragraphStyle: paragraphStyle, - .font: font, - .foregroundColor: color, - ] - return NSAttributedString(string: string, attributes: attributes) - } -} - -extension CGSize { - fileprivate func roundedUp() -> CGSize { - CGSize(width: width.rounded(.up), height: height.rounded(.up)) - } -} - -extension CGRect { - fileprivate init(padding: UIEdgeInsets, size: CGSize) { - self.init(origin: CGPoint(x: padding.left, y: padding.top), size: size) - } -} - -private func + (lhs: CGSize, rhs: UIEdgeInsets) -> CGSize { - return CGSize(width: lhs.width + rhs.left + rhs.right, height: lhs.height + rhs.top + rhs.bottom) -} - -private func - (lhs: CGSize, rhs: UIEdgeInsets) -> CGSize { - return CGSize(width: lhs.width - rhs.left - rhs.right, height: lhs.height - rhs.top - rhs.bottom) -} - -extension UIBezierPath { - fileprivate static func calloutPath( - size: CGSize, - tailSize: CGFloat, - cornerRadius: CGFloat, - anchor: ViewAnnotationAnchor - ) -> UIBezierPath { - let rect = CGRect(origin: .init(x: 0, y: 0), size: size) - let bubbleRect = rect.insetBy(dx: tailSize, dy: tailSize) - - let path = UIBezierPath( - roundedRect: bubbleRect, - cornerRadius: cornerRadius - ) - - let tailPath = UIBezierPath() - let p = tailSize - let h = size.height - let w = size.width - let r = cornerRadius - let tailPoints: [CGPoint] = switch anchor { - case .topLeft: - [CGPoint(x: 0, y: 0), CGPoint(x: p + r, y: p), CGPoint(x: p, y: p + r)] - case .top: - [CGPoint(x: w / 2, y: 0), CGPoint(x: w / 2 - p, y: p), CGPoint(x: w / 2 + p, y: p)] - case .topRight: - [CGPoint(x: w, y: 0), CGPoint(x: w - p, y: p + r), CGPoint(x: w - 3 * p, y: p)] - case .bottomLeft: - [CGPoint(x: 0, y: h), CGPoint(x: p, y: h - (p + r)), CGPoint(x: p + r, y: h - p)] - case .bottom: - [CGPoint(x: w / 2, y: h), CGPoint(x: w / 2 - p, y: h - p), CGPoint(x: w / 2 + p, y: h - p)] - case .bottomRight: - [CGPoint(x: w, y: h), CGPoint(x: w - (p + r), y: h - p), CGPoint(x: w - p, y: h - (p + r))] - case .left: - [CGPoint(x: 0, y: h / 2), CGPoint(x: p, y: h / 2 - p), CGPoint(x: p, y: h / 2 + p)] - case .right: - [CGPoint(x: w, y: h / 2), CGPoint(x: w - p, y: h / 2 - p), CGPoint(x: w - p, y: h / 2 + p)] - default: - [] - } - - for (i, point) in tailPoints.enumerated() { - if i == 0 { - tailPath.move(to: point) - } else { - tailPath.addLine(to: point) - } - } - tailPath.close() - path.append(tailPath) - return path - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift deleted file mode 100644 index 75d9b5c17..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift +++ /dev/null @@ -1,139 +0,0 @@ -import Foundation -import MapboxDirections -import MapboxMaps - -struct ETAViewsAnnotationFeature: MapFeature { - var id: String - - private let viewAnnotations: [ViewAnnotation] - - init( - for navigationRoutes: NavigationRoutes, - showMainRoute: Bool, - showAlternatives: Bool, - isRelative: Bool, - annotateAtManeuver: Bool, - mapStyleConfig: MapStyleConfig - ) { - let routesContainTolls = navigationRoutes.alternativeRoutes.contains { - ($0.route.tollIntersections?.count ?? 0) > 0 - } - var featureId = "" - - var annotations = [ViewAnnotation]() - if showMainRoute { - featureId += navigationRoutes.mainRoute.routeId.rawValue - let tollsHint = routesContainTolls ? navigationRoutes.mainRoute.route.containsTolls : nil - let etaView = ETAView( - eta: navigationRoutes.mainRoute.route.expectedTravelTime, - isSelected: true, - tollsHint: tollsHint, - mapStyleConfig: mapStyleConfig - ) - if let geometry = navigationRoutes.mainRoute.route.geometryForCallout() { - annotations.append( - ViewAnnotation( - annotatedFeature: .geometry(geometry), - view: etaView - ) - ) - } else { - annotations.append( - ViewAnnotation( - layerId: FeatureIds.RouteAnnotation.main.layerId, - view: etaView - ) - ) - } - } - if showAlternatives { - for (idx, alternativeRoute) in navigationRoutes.alternativeRoutes.enumerated() { - featureId += alternativeRoute.routeId.rawValue - let tollsHint = routesContainTolls ? alternativeRoute.route.containsTolls : nil - let etaView = if isRelative { - ETAView( - travelTimeDelta: alternativeRoute.expectedTravelTimeDelta, - tollsHint: tollsHint, - mapStyleConfig: mapStyleConfig - ) - } else { - ETAView( - eta: alternativeRoute.infoFromOrigin.duration, - isSelected: false, - tollsHint: tollsHint, - mapStyleConfig: mapStyleConfig - ) - } - let limit: Range - if annotateAtManeuver { - let deviationOffset = alternativeRoute.deviationOffset() - limit = (deviationOffset + 0.01)..<(deviationOffset + 0.05) - } else { - limit = 0.2..<0.8 - } - if let geometry = alternativeRoute.route.geometryForCallout(clampedTo: limit) { - annotations.append( - ViewAnnotation( - annotatedFeature: .geometry(geometry), - view: etaView - ) - ) - } else { - annotations.append( - ViewAnnotation( - layerId: FeatureIds.RouteAnnotation.alternative(index: idx).layerId, - view: etaView - ) - ) - } - } - } - annotations.forEach { - guard let etaView = $0.view as? ETAView else { return } - $0.setup(with: etaView) - } - self.id = featureId - self.viewAnnotations = annotations - } - - func add(to mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { - for annotation in viewAnnotations { - mapView.viewAnnotations.add(annotation) - } - } - - func remove(from mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { - viewAnnotations.forEach { $0.remove() } - } - - func update(oldValue: any MapFeature, in mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { - oldValue.remove(from: mapView, order: &order) - add(to: mapView, order: &order) - } -} - -extension Route { - fileprivate func geometryForCallout(clampedTo range: Range = 0.2..<0.8) -> Geometry? { - return shape?.trimmed( - from: distance * range.lowerBound, - to: distance * range.upperBound - )?.geometry - } - - fileprivate var containsTolls: Bool { - !(tollIntersections?.isEmpty ?? true) - } -} - -extension ViewAnnotation { - fileprivate func setup(with etaView: ETAView) { - ignoreCameraPadding = true - onAnchorChanged = { config in - etaView.anchor = config.anchor - } - variableAnchors = [ViewAnnotationAnchor.bottomLeft, .bottomRight, .topLeft, .topRight].map { - ViewAnnotationAnchorConfig(anchor: $0) - } - setNeedsUpdateSize() - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift deleted file mode 100644 index 2ddd6d67f..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift +++ /dev/null @@ -1,239 +0,0 @@ -import Foundation -import MapboxMaps -import Turf - -/// Simplifies source/layer/image managements for MapView -/// -/// ## Supported features: -/// -/// ### Layers -/// -/// Can be added/removed but not updated. Custom update logic can be performed using `onUpdate` callback. This -/// is done for performance reasons and to simplify implementation as map layers doesn't support equatable protocol. -/// If you want to update layers, you can consider assigning updated layer a new id. -/// -/// It there is only one source, layers will get it assigned automatically, overwise, layers should has source set -/// manually. -/// -/// ### Sources -/// -/// Sources can also be added/removed, but unlike layers, sources are always updated. -/// -/// -struct GeoJsonMapFeature: MapFeature { - struct Source { - let id: String - let geoJson: GeoJSONObject - } - - typealias LayerId = String - typealias SourceId = String - - let id: String - let sources: [SourceId: Source] - - let customizeSource: @MainActor (_ source: inout GeoJSONSource, _ id: String) -> Void - - let layers: [LayerId: any Layer] - - // MARK: Lifecycle callbacks - - let onBeforeAdd: @MainActor (_ mapView: MapView) -> Void - let onAfterAdd: @MainActor (_ mapView: MapView) -> Void - let onUpdate: @MainActor (_ mapView: MapView) throws -> Void - let onAfterUpdate: @MainActor (_ mapView: MapView) throws -> Void - let onAfterRemove: @MainActor (_ mapView: MapView) -> Void - - init( - id: String, - sources: [Source], - customizeSource: @escaping @MainActor (_: inout GeoJSONSource, _ id: String) -> Void, - layers: [any Layer], - onBeforeAdd: @escaping @MainActor (_: MapView) -> Void = { _ in }, - onAfterAdd: @escaping @MainActor (_: MapView) -> Void = { _ in }, - onUpdate: @escaping @MainActor (_: MapView) throws -> Void = { _ in }, - onAfterUpdate: @escaping @MainActor (_: MapView) throws -> Void = { _ in }, - onAfterRemove: @escaping @MainActor (_: MapView) -> Void = { _ in } - ) { - self.id = id - self.sources = Dictionary(uniqueKeysWithValues: sources.map { ($0.id, $0) }) - self.customizeSource = customizeSource - self.layers = Dictionary(uniqueKeysWithValues: layers.map { ($0.id, $0) }) - self.onBeforeAdd = onBeforeAdd - self.onAfterAdd = onAfterAdd - self.onUpdate = onUpdate - self.onAfterUpdate = onAfterUpdate - self.onAfterRemove = onAfterRemove - } - - // MARK: - MapFeature conformance - - @MainActor - func add(to mapView: MapView, order: inout MapLayersOrder) { - onBeforeAdd(mapView) - - let map: MapboxMap = mapView.mapboxMap - for (_, source) in sources { - addSource(source, to: map) - } - - for (_, var layer) in layers { - addLayer(&layer, to: map, order: &order) - } - - onAfterAdd(mapView) - } - - @MainActor - private func addLayer(_ layer: inout any Layer, to map: MapboxMap, order: inout MapLayersOrder) { - do { - if map.layerExists(withId: layer.id) { - try map.removeLayer(withId: layer.id) - } - order.insert(id: layer.id) - if let slot = order.slot(forId: layer.id), map.allSlotIdentifiers.contains(slot) { - layer.slot = slot - } - try map.addLayer(layer, layerPosition: order.position(forId: layer.id)) - } catch { - Log.error("Failed to add layer '\(layer.id)': \(error)", category: .navigationUI) - } - } - - @MainActor - private func addSource(_ source: Source, to map: MapboxMap) { - do { - if map.sourceExists(withId: source.id) { - map.updateGeoJSONSource( - withId: source.id, - geoJSON: source.geoJson - ) - } else { - var geoJsonSource = GeoJSONSource(id: source.id) - geoJsonSource.data = source.geoJson.sourceData - customizeSource(&geoJsonSource, source.id) - try map.addSource(geoJsonSource) - } - } catch { - Log.error("Failed to add source '\(source.id)': \(error)", category: .navigationUI) - } - } - - @MainActor - func update(oldValue: any MapFeature, in mapView: MapView, order: inout MapLayersOrder) { - guard let oldValue = oldValue as? Self else { - preconditionFailure("Incorrect type passed for oldValue") - } - - for (_, source) in sources { - guard mapView.mapboxMap.sourceExists(withId: source.id) - else { - // In case the map style was changed and the source is missing we're re-adding it back. - oldValue.remove(from: mapView, order: &order) - remove(from: mapView, order: &order) - add(to: mapView, order: &order) - return - } - } - - do { - try onUpdate(mapView) - let map: MapboxMap = mapView.mapboxMap - - let diff = diff(oldValue: oldValue, newValue: self) - for var addedLayer in diff.addedLayers { - addLayer(&addedLayer, to: map, order: &order) - } - for removedLayer in diff.removedLayers { - removeLayer(removedLayer, from: map, order: &order) - } - for addedSource in diff.addedSources { - addSource(addedSource, to: map) - } - for removedSource in diff.removedSources { - removeSource(removedSource.id, from: map) - } - - for (_, source) in sources { - mapView.mapboxMap.updateGeoJSONSource( - withId: source.id, - geoJSON: source.geoJson - ) - } - try onAfterUpdate(mapView) - } catch { - Log.error("Failed to update map feature '\(id)': \(error)", category: .navigationUI) - } - } - - @MainActor - func remove(from mapView: MapView, order: inout MapLayersOrder) { - let map: MapboxMap = mapView.mapboxMap - - for (_, layer) in layers { - removeLayer(layer, from: map, order: &order) - } - - for sourceId in sources.keys { - removeSource(sourceId, from: map) - } - - onAfterRemove(mapView) - } - - @MainActor - private func removeLayer(_ layer: any Layer, from map: MapboxMap, order: inout MapLayersOrder) { - guard map.layerExists(withId: layer.id) else { return } - do { - try map.removeLayer(withId: layer.id) - order.remove(id: layer.id) - } catch { - Log.error("Failed to remove layer '\(layer.id)': \(error)", category: .navigationUI) - } - } - - @MainActor - private func removeSource(_ sourceId: SourceId, from map: MapboxMap) { - if map.sourceExists(withId: sourceId) { - do { - try map.removeSource(withId: sourceId) - } catch { - Log.error("Failed to remove source '\(sourceId)': \(error)", category: .navigationUI) - } - } - } - - // MARK: Diff - - private struct Diff { - let addedLayers: [any Layer] - let removedLayers: [any Layer] - let addedSources: [Source] - let removedSources: [Source] - } - - private func diff(oldValue: Self, newValue: Self) -> Diff { - .init( - addedLayers: newValue.layers.filter { oldValue.layers[$0.key] == nil }.map(\.value), - removedLayers: oldValue.layers.filter { newValue.layers[$0.key] == nil }.map(\.value), - addedSources: newValue.sources.filter { oldValue.sources[$0.key] == nil }.map(\.value), - removedSources: oldValue.sources.filter { newValue.sources[$0.key] == nil }.map(\.value) - ) - } -} - -// MARK: Helpers - -extension GeoJSONObject { - /// Ported from MapboxMaps as the same var is internal in the SDK. - fileprivate var sourceData: GeoJSONSourceData { - switch self { - case .geometry(let geometry): - return .geometry(geometry) - case .feature(let feature): - return .feature(feature) - case .featureCollection(let collection): - return .featureCollection(collection) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift deleted file mode 100644 index fcd831ada..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import MapboxMaps - -/// Something that can be added/removed/updated in MapboxMaps.MapView. -/// -/// Use ``MapFeaturesStore`` to manage a set of features. -protocol MapFeature { - var id: String { get } - - @MainActor - func add(to mapView: MapView, order: inout MapLayersOrder) - @MainActor - func remove(from mapView: MapView, order: inout MapLayersOrder) - @MainActor - func update(oldValue: MapFeature, in mapView: MapView, order: inout MapLayersOrder) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift deleted file mode 100644 index 1f5cb49fb..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift +++ /dev/null @@ -1,117 +0,0 @@ -import Foundation -import MapboxMaps - -/// A store for ``MapFeature``s. -/// -/// It handle style reload by re-adding currently active features (make sure you call `styleLoaded` method). -/// Use `update(using:)` method to provide a new snapshot of features that are managed by this store. The store will -/// handle updates/removes/additions to the map view. -@MainActor -final class MapFeaturesStore { - private struct Features: Sequence { - private var features: [String: any MapFeature] = [:] - - func makeIterator() -> some IteratorProtocol { - features.values.makeIterator() - } - - subscript(_ id: String) -> (any MapFeature)? { - features[id] - } - - mutating func insert(_ feature: any MapFeature) { - features[feature.id] = feature - } - - mutating func remove(_ feature: any MapFeature) -> (any MapFeature)? { - features.removeValue(forKey: feature.id) - } - - mutating func removeAll() -> some Sequence { - let allFeatures = features.values - features = [:] - return allFeatures - } - } - - private let mapView: MapView - private var styleLoadSubscription: MapboxMaps.Cancelable? - private var features: Features = .init() - - private var currentStyleLoaded: Bool = false - private var currentStyleUri: StyleURI? - - private var styleLoaded: Bool { - if currentStyleUri != mapView.mapboxMap.styleURI { - currentStyleLoaded = false - } - return currentStyleLoaded - } - - init(mapView: MapView) { - self.mapView = mapView - self.currentStyleUri = mapView.mapboxMap.styleURI - self.currentStyleLoaded = mapView.mapboxMap.isStyleLoaded - } - - func deactivate(order: inout MapLayersOrder) { - styleLoadSubscription?.cancel() - guard styleLoaded else { return } - features.forEach { $0.remove(from: mapView, order: &order) } - } - - func update(using allFeatures: [any MapFeature]?, order: inout MapLayersOrder) { - guard let allFeatures, !allFeatures.isEmpty else { - removeAll(order: &order); return - } - - let newFeatureIds = Set(allFeatures.map(\.id)) - for existingFeature in features where !newFeatureIds.contains(existingFeature.id) { - remove(existingFeature, order: &order) - } - - for feature in allFeatures { - update(feature, order: &order) - } - } - - private func removeAll(order: inout MapLayersOrder) { - let allFeatures = features.removeAll() - guard styleLoaded else { return } - - for feature in allFeatures { - feature.remove(from: mapView, order: &order) - } - } - - private func update(_ feature: any MapFeature, order: inout MapLayersOrder) { - defer { - features.insert(feature) - } - - guard styleLoaded else { return } - - if let oldFeature = features[feature.id] { - feature.update(oldValue: oldFeature, in: mapView, order: &order) - } else { - feature.add(to: mapView, order: &order) - } - } - - private func remove(_ feature: some MapFeature, order: inout MapLayersOrder) { - guard let removeFeature = features.remove(feature) else { return } - - if styleLoaded { - removeFeature.remove(from: mapView, order: &order) - } - } - - func styleLoaded(order: inout MapLayersOrder) { - currentStyleUri = mapView.mapboxMap.styleURI - currentStyleLoaded = true - - for feature in features { - feature.add(to: mapView, order: &order) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift deleted file mode 100644 index 830f23d92..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift +++ /dev/null @@ -1,38 +0,0 @@ -import MapboxMaps - -extension MapboxMap { - /// Adds image to style if it doesn't exist already and log any errors that occur. - func provisionImage(id: String, _ addImageToMap: (MapboxMap) throws -> Void) { - if !imageExists(withId: id) { - do { - try addImageToMap(self) - } catch { - Log.error("Failed to add image (id: \(id)) to style with error \(error)", category: .navigationUI) - } - } - } - - func setRouteLineOffset( - _ offset: Double, - for routeLineIds: FeatureIds.RouteLine - ) { - guard offset >= 0.0 else { return } - do { - let layerIds: [String] = [ - routeLineIds.main, - routeLineIds.casing, - routeLineIds.restrictedArea, - ] - - for layerId in layerIds where layerExists(withId: layerId) { - try setLayerProperty( - for: layerId, - property: "line-trim-offset", - value: [0.0, Double.minimum(1.0, offset)] - ) - } - } catch { - Log.error("Failed to update route line gradient with error: \(error)", category: .navigationUI) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapLayersOrder.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapLayersOrder.swift deleted file mode 100644 index bdb23e842..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/MapLayersOrder.swift +++ /dev/null @@ -1,260 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxMaps - -/// Allows to order layers with easy by defining order rules and then query order for any added layer. -struct MapLayersOrder { - @resultBuilder - enum Builder { - static func buildPartialBlock(first rule: Rule) -> [Rule] { - [rule] - } - - static func buildPartialBlock(first slottedRules: SlottedRules) -> [Rule] { - slottedRules.rules - } - - static func buildPartialBlock(accumulated rules: [Rule], next rule: Rule) -> [Rule] { - with(rules) { - $0.append(rule) - } - } - - static func buildPartialBlock(accumulated rules: [Rule], next slottedRules: SlottedRules) -> [Rule] { - rules + slottedRules.rules - } - } - - struct SlottedRules { - let rules: [MapLayersOrder.Rule] - - init(_ slot: Slot?, @MapLayersOrder.Builder rules: () -> [Rule]) { - self.rules = rules().map { rule in - with(rule) { $0.slot = slot } - } - } - } - - struct Rule { - struct MatchPredicate { - let block: (String) -> Bool - - static func hasPrefix(_ prefix: String) -> Self { - .init { - $0.hasPrefix(prefix) - } - } - - static func contains(_ substring: String) -> Self { - .init { - $0.contains(substring) - } - } - - static func exact(_ id: String) -> Self { - .init { - $0 == id - } - } - - static func any(of ids: any Sequence) -> Self { - let set = Set(ids) - return .init { - set.contains($0) - } - } - } - - struct OrderedAscendingComparator { - let block: (_ lhs: String, _ rhs: String) -> Bool - - static func constant(_ value: Bool) -> Self { - .init { _, _ in - value - } - } - - static func order(_ ids: [String]) -> Self { - return .init { lhs, rhs in - guard let lhsIndex = ids.firstIndex(of: lhs), - let rhsIndex = ids.firstIndex(of: rhs) else { return true } - return lhsIndex < rhsIndex - } - } - } - - let matches: (String) -> Bool - let isOrderedAscending: (_ lhs: String, _ rhs: String) -> Bool - var slot: Slot? - - init( - predicate: MatchPredicate, - isOrderedAscending: OrderedAscendingComparator - ) { - self.matches = predicate.block - self.isOrderedAscending = isOrderedAscending.block - } - - static func hasPrefix( - _ prefix: String, - isOrderedAscending: OrderedAscendingComparator = .constant(true) - ) -> Rule { - Rule(predicate: .hasPrefix(prefix), isOrderedAscending: isOrderedAscending) - } - - static func contains( - _ substring: String, - isOrderedAscending: OrderedAscendingComparator = .constant(true) - ) -> Rule { - Rule(predicate: .contains(substring), isOrderedAscending: isOrderedAscending) - } - - static func exact( - _ id: String, - isOrderedAscending: OrderedAscendingComparator = .constant(true) - ) -> Rule { - Rule(predicate: .exact(id), isOrderedAscending: isOrderedAscending) - } - - static func orderedIds(_ ids: [String]) -> Rule { - return Rule( - predicate: .any(of: ids), - isOrderedAscending: .order(ids) - ) - } - - func slotted(_ slot: Slot) -> Self { - with(self) { - $0.slot = slot - } - } - } - - /// Ids that are managed by map style. - private var styleIds: [String] = [] - /// Ids that are managed by SDK. - private var customIds: Set = [] - /// Merged `styleIds` and `customIds` in order defined by rules. - private var orderedIds: [String] = [] - /// A map from id to position in `orderedIds` to speed up `position(forId:)` query. - private var orderedIdsIndices: [String: Int] = [:] - private var idToSlot: [String: Slot] = [:] - /// Ordered list of rules that define order. - private let rules: [Rule] - - /// Used for styles with no slots support. - private let legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? - - init( - @MapLayersOrder.Builder builder: () -> [Rule], - legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? - ) { - self.rules = builder() - self.legacyPosition = legacyPosition - } - - /// Inserts a new id and makes it possible to use it in `position(forId:)` method. - mutating func insert(id: String) { - customIds.insert(id) - - guard let ruleIndex = rules.firstIndex(where: { $0.matches(id) }) else { - orderedIds.append(id) - orderedIdsIndices[id] = orderedIds.count - 1 - return - } - - func binarySearch() -> Int { - var left = 0 - var right = orderedIds.count - - while left < right { - let mid = left + (right - left) / 2 - if let currentRuleIndex = rules.firstIndex(where: { $0.matches(orderedIds[mid]) }) { - if currentRuleIndex > ruleIndex { - right = mid - } else if currentRuleIndex == ruleIndex { - if !rules[ruleIndex].isOrderedAscending(orderedIds[mid], id) { - right = mid - } else { - left = mid + 1 - } - } else { - left = mid + 1 - } - } else { - right = mid - } - } - return left - } - - let insertionIndex = binarySearch() - orderedIds.insert(id, at: insertionIndex) - - // Update the indices of the elements after the insertion point - for index in insertionIndex.. LayerPosition? { - if let legacyPosition { - return legacyPosition(id) - } - - guard let index = orderedIdsIndices[id] else { return nil } - let belowId = index == 0 ? nil : orderedIds[index - 1] - let aboveId = index == orderedIds.count - 1 ? nil : orderedIds[index + 1] - - if let belowId { - return .above(belowId) - } else if let aboveId { - return .below(aboveId) - } else { - return nil - } - } - - func slot(forId id: String) -> Slot? { - idToSlot[id] - } - - private func rule(matching id: String) -> Rule? { - rules.first { rule in - rule.matches(id) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift deleted file mode 100644 index d4eeabeca..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift +++ /dev/null @@ -1,537 +0,0 @@ -import Combine -import MapboxDirections -@_spi(Experimental) import MapboxMaps -import enum SwiftUI.ColorScheme -import UIKit - -struct CustomizedLayerProvider { - var customizedLayer: (Layer) -> Layer -} - -struct MapStyleConfig: Equatable { - var routeCasingColor: UIColor - var routeAlternateCasingColor: UIColor - var routeRestrictedAreaColor: UIColor - var traversedRouteColor: UIColor? - var maneuverArrowColor: UIColor - var maneuverArrowStrokeColor: UIColor - - var routeAnnotationSelectedColor: UIColor - var routeAnnotationColor: UIColor - var routeAnnotationSelectedTextColor: UIColor - var routeAnnotationTextColor: UIColor - var routeAnnotationMoreTimeTextColor: UIColor - var routeAnnotationLessTimeTextColor: UIColor - var routeAnnotationTextFont: UIFont - - var routeLineTracksTraversal: Bool - var isRestrictedAreaEnabled: Bool - var showsTrafficOnRouteLine: Bool - var showsAlternatives: Bool - var showsIntermediateWaypoints: Bool - var occlusionFactor: Value? - var congestionConfiguration: CongestionConfiguration - - var waypointColor: UIColor - var waypointStrokeColor: UIColor -} - -/// Manages all the sources/layers used in NavigationMap. -@MainActor -final class NavigationMapStyleManager { - private let mapView: MapView - private var lifetimeSubscriptions: Set = [] - private var layersOrder: MapLayersOrder - private var layerIds: [String] - - var customizedLayerProvider: CustomizedLayerProvider = .init { $0 } - var customRouteLineLayerPosition: LayerPosition? - - private let routeFeaturesStore: MapFeaturesStore - private let waypointFeaturesStore: MapFeaturesStore - private let arrowFeaturesStore: MapFeaturesStore - private let voiceInstructionFeaturesStore: MapFeaturesStore - private let intersectionAnnotationsFeaturesStore: MapFeaturesStore - private let routeAnnotationsFeaturesStore: MapFeaturesStore - private let routeAlertsFeaturesStore: MapFeaturesStore - - init(mapView: MapView, customRouteLineLayerPosition: LayerPosition?) { - self.mapView = mapView - self.layersOrder = Self.makeMapLayersOrder( - with: mapView, - customRouteLineLayerPosition: customRouteLineLayerPosition - ) - self.layerIds = mapView.mapboxMap.allLayerIdentifiers.map(\.id) - self.routeFeaturesStore = .init(mapView: mapView) - self.waypointFeaturesStore = .init(mapView: mapView) - self.arrowFeaturesStore = .init(mapView: mapView) - self.voiceInstructionFeaturesStore = .init(mapView: mapView) - self.intersectionAnnotationsFeaturesStore = .init(mapView: mapView) - self.routeAnnotationsFeaturesStore = .init(mapView: mapView) - self.routeAlertsFeaturesStore = .init(mapView: mapView) - - mapView.mapboxMap.onStyleLoaded.sink { [weak self] _ in - self?.onStyleLoaded() - }.store(in: &lifetimeSubscriptions) - } - - func onStyleLoaded() { - // MapsSDK removes all layers when a style is loaded, so we have to recreate MapLayersOrder. - layersOrder = Self.makeMapLayersOrder(with: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) - layerIds = mapView.mapboxMap.allLayerIdentifiers.map(\.id) - layersOrder.setStyleIds(layerIds) - - routeFeaturesStore.styleLoaded(order: &layersOrder) - waypointFeaturesStore.styleLoaded(order: &layersOrder) - arrowFeaturesStore.styleLoaded(order: &layersOrder) - voiceInstructionFeaturesStore.styleLoaded(order: &layersOrder) - intersectionAnnotationsFeaturesStore.styleLoaded(order: &layersOrder) - routeAnnotationsFeaturesStore.styleLoaded(order: &layersOrder) - routeAlertsFeaturesStore.styleLoaded(order: &layersOrder) - } - - func updateRoutes( - _ routes: NavigationRoutes, - config: MapStyleConfig, - featureProvider: RouteLineFeatureProvider - ) { - routeFeaturesStore.update( - using: routeLineMapFeatures( - routes: routes, - config: config, - featureProvider: featureProvider, - customizedLayerProvider: customizedLayerProvider - ), - order: &layersOrder - ) - } - - func updateWaypoints( - route: Route, - legIndex: Int, - config: MapStyleConfig, - featureProvider: WaypointFeatureProvider - ) { - let waypoints = route.waypointsMapFeature( - mapView: mapView, - legIndex: legIndex, - config: config, - featureProvider: featureProvider, - customizedLayerProvider: customizedLayerProvider - ) - waypointFeaturesStore.update( - using: waypoints.map { [$0] } ?? [], - order: &layersOrder - ) - } - - func updateArrows( - route: Route, - legIndex: Int, - stepIndex: Int, - config: MapStyleConfig - ) { - guard route.containsStep(at: legIndex, stepIndex: stepIndex) - else { - removeArrows(); return - } - - arrowFeaturesStore.update( - using: route.maneuverArrowMapFeatures( - ids: .nextArrow(), - cameraZoom: mapView.mapboxMap.cameraState.zoom, - legIndex: legIndex, - stepIndex: stepIndex, - config: config, - customizedLayerProvider: customizedLayerProvider - ), - order: &layersOrder - ) - } - - func updateVoiceInstructions(route: Route) { - voiceInstructionFeaturesStore.update( - using: route.voiceInstructionMapFeatures( - ids: .init(), - customizedLayerProvider: customizedLayerProvider - ), - order: &layersOrder - ) - } - - func updateIntersectionAnnotations(routeProgress: RouteProgress) { - intersectionAnnotationsFeaturesStore.update( - using: routeProgress.intersectionAnnotationsMapFeatures( - ids: .currentRoute, - customizedLayerProvider: customizedLayerProvider - ), - order: &layersOrder - ) - } - - func updateRouteAnnotations( - navigationRoutes: NavigationRoutes, - annotationKinds: Set, - config: MapStyleConfig - ) { - routeAnnotationsFeaturesStore.update( - using: navigationRoutes.routeDurationMapFeatures( - annotationKinds: annotationKinds, - config: config - ), - order: &layersOrder - ) - } - - func updateRouteAlertsAnnotations( - navigationRoutes: NavigationRoutes, - excludedRouteAlertTypes: RoadAlertType, - distanceTraveled: CLLocationDistance = 0.0 - ) { - routeAlertsFeaturesStore.update( - using: navigationRoutes.routeAlertsAnnotationsMapFeatures( - ids: .default, - distanceTraveled: distanceTraveled, - customizedLayerProvider: customizedLayerProvider, - excludedRouteAlertTypes: excludedRouteAlertTypes - ), - order: &layersOrder - ) - } - - func updateFreeDriveAlertsAnnotations( - roadObjects: [RoadObjectAhead], - excludedRouteAlertTypes: RoadAlertType, - distanceTraveled: CLLocationDistance = 0.0 - ) { - guard !roadObjects.isEmpty else { - return removeRoadAlertsAnnotations() - } - routeAlertsFeaturesStore.update( - using: roadObjects.routeAlertsAnnotationsMapFeatures( - ids: .default, - distanceTraveled: distanceTraveled, - customizedLayerProvider: customizedLayerProvider, - excludedRouteAlertTypes: excludedRouteAlertTypes - ), - order: &layersOrder - ) - } - - func removeRoutes() { - routeFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeWaypoints() { - waypointFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeArrows() { - arrowFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeVoiceInstructions() { - voiceInstructionFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeIntersectionAnnotations() { - intersectionAnnotationsFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeRouteAnnotations() { - routeAnnotationsFeaturesStore.update(using: nil, order: &layersOrder) - } - - private func removeRoadAlertsAnnotations() { - routeAlertsFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeAllFeatures() { - removeRoutes() - removeWaypoints() - removeArrows() - removeVoiceInstructions() - removeIntersectionAnnotations() - removeRouteAnnotations() - removeRoadAlertsAnnotations() - } - - private func routeLineMapFeatures( - routes: NavigationRoutes, - config: MapStyleConfig, - featureProvider: RouteLineFeatureProvider, - customizedLayerProvider: CustomizedLayerProvider - ) -> [any MapFeature] { - var features: [any MapFeature] = [] - - features.append(contentsOf: routes.mainRoute.route.routeLineMapFeatures( - ids: .main, - offset: 0, - isSoftGradient: true, - isAlternative: false, - config: config, - featureProvider: featureProvider, - customizedLayerProvider: customizedLayerProvider - )) - - if config.showsAlternatives { - for (idx, alternativeRoute) in routes.alternativeRoutes.enumerated() { - let deviationOffset = alternativeRoute.deviationOffset() - features.append(contentsOf: alternativeRoute.route.routeLineMapFeatures( - ids: .alternative(idx: idx), - offset: deviationOffset, - isSoftGradient: true, - isAlternative: true, - config: config, - featureProvider: featureProvider, - customizedLayerProvider: customizedLayerProvider - )) - } - } - - return features - } - - func setRouteLineOffset( - _ offset: Double, - for routeLineIds: FeatureIds.RouteLine - ) { - mapView.mapboxMap.setRouteLineOffset(offset, for: routeLineIds) - } - - private static func makeMapLayersOrder( - with mapView: MapView, - customRouteLineLayerPosition: LayerPosition? - ) -> MapLayersOrder { - let alternative_0_ids = FeatureIds.RouteLine.alternative(idx: 0) - let alternative_1_ids = FeatureIds.RouteLine.alternative(idx: 1) - let mainLineIds = FeatureIds.RouteLine.main - let arrowIds = FeatureIds.ManeuverArrow.nextArrow() - let waypointIds = FeatureIds.RouteWaypoints.default - let voiceInstructionIds = FeatureIds.VoiceInstruction.currentRoute - let intersectionIds = FeatureIds.IntersectionAnnotation.currentRoute - let routeAlertIds = FeatureIds.RouteAlertAnnotation.default - typealias R = MapLayersOrder.Rule - typealias SlottedRules = MapLayersOrder.SlottedRules - - let allSlotIdentifiers = mapView.mapboxMap.allSlotIdentifiers - let containsMiddleSlot = Slot.middle.map(allSlotIdentifiers.contains) ?? false - let legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? = containsMiddleSlot ? nil : { - legacyLayerPosition(for: $0, mapView: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) - } - - return MapLayersOrder( - builder: { - SlottedRules(.middle) { - R.orderedIds([ - alternative_0_ids.casing, - alternative_0_ids.main, - ]) - R.orderedIds([ - alternative_1_ids.casing, - alternative_1_ids.main, - ]) - R.orderedIds([ - mainLineIds.traversedRoute, - mainLineIds.casing, - mainLineIds.main, - ]) - R.orderedIds([ - arrowIds.arrowStroke, - arrowIds.arrow, - arrowIds.arrowSymbolCasing, - arrowIds.arrowSymbol, - ]) - R.orderedIds([ - alternative_0_ids.restrictedArea, - alternative_1_ids.restrictedArea, - mainLineIds.restrictedArea, - ]) - /// To show on top of arrows - R.hasPrefix("poi") - R.orderedIds([ - voiceInstructionIds.layer, - voiceInstructionIds.circleLayer, - ]) - } - // Setting the top position on the map. We cannot explicitly set `.top` position because `.top` - // renders behind Place and Transit labels - SlottedRules(nil) { - R.orderedIds([ - intersectionIds.layer, - routeAlertIds.layer, - waypointIds.innerCircle, - waypointIds.markerIcon, - NavigationMapView.LayerIdentifier.puck2DLayer, - NavigationMapView.LayerIdentifier.puck3DLayer, - ]) - } - }, - legacyPosition: legacyPosition - ) - } - - private static func legacyLayerPosition( - for layerIdentifier: String, - mapView: MapView, - customRouteLineLayerPosition: LayerPosition? - ) -> MapboxMaps.LayerPosition? { - let mainLineIds = FeatureIds.RouteLine.main - if layerIdentifier.hasPrefix(mainLineIds.main), - let customRouteLineLayerPosition, - !mapView.mapboxMap.allLayerIdentifiers.contains(where: { $0.id.hasPrefix(mainLineIds.main) }) - { - return customRouteLineLayerPosition - } - - let alternative_0_ids = FeatureIds.RouteLine.alternative(idx: 0) - let alternative_1_ids = FeatureIds.RouteLine.alternative(idx: 1) - let arrowIds = FeatureIds.ManeuverArrow.nextArrow() - let waypointIds = FeatureIds.RouteWaypoints.default - let voiceInstructionIds = FeatureIds.VoiceInstruction.currentRoute - let intersectionIds = FeatureIds.IntersectionAnnotation.currentRoute - let routeAlertIds = FeatureIds.RouteAlertAnnotation.default - - let lowermostSymbolLayers: [String] = [ - alternative_0_ids.casing, - alternative_0_ids.main, - alternative_1_ids.casing, - alternative_1_ids.main, - mainLineIds.traversedRoute, - mainLineIds.casing, - mainLineIds.main, - mainLineIds.restrictedArea, - ].compactMap { $0 } - let aboveRoadLayers: [String] = [ - arrowIds.arrowStroke, - arrowIds.arrow, - arrowIds.arrowSymbolCasing, - arrowIds.arrowSymbol, - intersectionIds.layer, - routeAlertIds.layer, - waypointIds.innerCircle, - waypointIds.markerIcon, - ] - let uppermostSymbolLayers: [String] = [ - voiceInstructionIds.layer, - voiceInstructionIds.circleLayer, - NavigationMapView.LayerIdentifier.puck2DLayer, - NavigationMapView.LayerIdentifier.puck3DLayer, - ] - let isLowermostLayer = lowermostSymbolLayers.contains(layerIdentifier) - let isAboveRoadLayer = aboveRoadLayers.contains(layerIdentifier) - let allAddedLayers: [String] = lowermostSymbolLayers + aboveRoadLayers + uppermostSymbolLayers - - var layerPosition: MapboxMaps.LayerPosition? - var lowerLayers = Set() - var upperLayers = Set() - var targetLayer: String? - - if let index = allAddedLayers.firstIndex(of: layerIdentifier) { - lowerLayers = Set(allAddedLayers.prefix(upTo: index)) - if allAddedLayers.indices.contains(index + 1) { - upperLayers = Set(allAddedLayers.suffix(from: index + 1)) - } - } - - var foundAboveLayer = false - for layerInfo in mapView.mapboxMap.allLayerIdentifiers.reversed() { - if lowerLayers.contains(layerInfo.id) { - // find the topmost layer that should be below the layerIdentifier. - if !foundAboveLayer { - layerPosition = .above(layerInfo.id) - foundAboveLayer = true - } - } else if upperLayers.contains(layerInfo.id) { - // find the bottommost layer that should be above the layerIdentifier. - layerPosition = .below(layerInfo.id) - } else if isLowermostLayer { - // find the topmost non symbol layer for layerIdentifier in lowermostSymbolLayers. - if targetLayer == nil, - layerInfo.type.rawValue != "symbol", - let sourceLayer = mapView.mapboxMap.layerProperty(for: layerInfo.id, property: "source-layer") - .value as? String, - !sourceLayer.isEmpty - { - if layerInfo.type.rawValue == "circle", - let isPersistentCircle = try? mapView.mapboxMap.isPersistentLayer(id: layerInfo.id) - { - let pitchAlignment = mapView.mapboxMap.layerProperty( - for: layerInfo.id, - property: "circle-pitch-alignment" - ).value as? String - if isPersistentCircle || (pitchAlignment != "map") { - continue - } - } - targetLayer = layerInfo.id - } - } else if isAboveRoadLayer { - // find the topmost road name label layer for layerIdentifier in arrowLayers. - if targetLayer == nil, - layerInfo.id.contains("road-label"), - mapView.mapboxMap.layerExists(withId: layerInfo.id) - { - targetLayer = layerInfo.id - } - } else { - // find the topmost layer for layerIdentifier in uppermostSymbolLayers. - if targetLayer == nil, - let sourceLayer = mapView.mapboxMap.layerProperty(for: layerInfo.id, property: "source-layer") - .value as? String, - !sourceLayer.isEmpty - { - targetLayer = layerInfo.id - } - } - } - - guard let targetLayer else { return layerPosition } - guard let layerPosition else { return .above(targetLayer) } - - if isLowermostLayer { - // For layers should be below symbol layers. - if case .below(let sequenceLayer) = layerPosition, !lowermostSymbolLayers.contains(sequenceLayer) { - // If the sequenceLayer isn't in lowermostSymbolLayers, it's above symbol layer. - // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost non symbol - // layer, - // but under the symbol layers. - return .above(targetLayer) - } - } else if isAboveRoadLayer { - // For layers should be above road name labels but below other symbol layers. - if case .below(let sequenceLayer) = layerPosition, uppermostSymbolLayers.contains(sequenceLayer) { - // If the sequenceLayer is in uppermostSymbolLayers, it's above all symbol layers. - // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost road name - // symbol layer. - return .above(targetLayer) - } else if case .above(let sequenceLayer) = layerPosition, lowermostSymbolLayers.contains(sequenceLayer) { - // If the sequenceLayer is in lowermostSymbolLayers, it's below all symbol layers. - // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost road name - // symbol layer. - return .above(targetLayer) - } - } else { - // For other layers should be uppermost and above symbol layers. - if case .above(let sequenceLayer) = layerPosition, !uppermostSymbolLayers.contains(sequenceLayer) { - // If the sequenceLayer isn't in uppermostSymbolLayers, it's below some symbol layers. - // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost layer. - return .above(targetLayer) - } - } - - return layerPosition - } -} - -extension NavigationMapStyleManager { - // TODO: These ids are specific to Standard style, we should allow customers to customize this - var poiLayerIds: [String] { - let poiLayerIds = layerIds.filter { layerId in - NavigationMapView.LayerIdentifier.clickablePoiLabels.contains { - layerId.hasPrefix($0) - } - } - return Array(poiLayerIds) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift deleted file mode 100644 index 93f376473..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift +++ /dev/null @@ -1,364 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxDirections -import MapboxMaps -import MapboxNavigationNative -import enum SwiftUI.ColorScheme -import UIKit - -extension NavigationRoutes { - func routeAlertsAnnotationsMapFeatures( - ids: FeatureIds.RouteAlertAnnotation, - distanceTraveled: CLLocationDistance, - customizedLayerProvider: CustomizedLayerProvider, - excludedRouteAlertTypes: RoadAlertType - ) -> [MapFeature] { - let convertedRouteAlerts = mainRoute.nativeRoute.getRouteInfo().alerts.map { - RoadObjectAhead( - roadObject: RoadObject($0.roadObject), - distance: $0.distanceToStart - ) - } - - return convertedRouteAlerts.routeAlertsAnnotationsMapFeatures( - ids: ids, - distanceTraveled: distanceTraveled, - customizedLayerProvider: customizedLayerProvider, - excludedRouteAlertTypes: excludedRouteAlertTypes - ) - } -} - -extension [RoadObjectAhead] { - func routeAlertsAnnotationsMapFeatures( - ids: FeatureIds.RouteAlertAnnotation, - distanceTraveled: CLLocationDistance, - customizedLayerProvider: CustomizedLayerProvider, - excludedRouteAlertTypes: RoadAlertType - ) -> [MapFeature] { - let featureCollection = FeatureCollection(features: roadObjectsFeatures( - for: self, - currentDistance: distanceTraveled, - excludedRouteAlertTypes: excludedRouteAlertTypes - )) - let layers: [any Layer] = [ - with(SymbolLayer(id: ids.layer, source: ids.source)) { - $0.iconImage = .expression(Exp(.get) { RoadObjectInfo.objectImageType }) - $0.minZoom = 10 - - $0.iconSize = .expression( - Exp(.interpolate) { - Exp(.linear) - Exp(.zoom) - Self.interpolationFactors.mapValues { $0 * 0.2 } - } - ) - - $0.iconColor = .expression(Exp(.get) { RoadObjectInfo.objectColor }) - }, - ] - return [ - GeoJsonMapFeature( - id: ids.featureId, - sources: [ - .init( - id: ids.source, - geoJson: .featureCollection(featureCollection) - ), - ], - customizeSource: { _, _ in }, - layers: layers.map { customizedLayerProvider.customizedLayer($0) }, - onBeforeAdd: { mapView in - Self.upsertRouteAlertsSymbolImages( - map: mapView.mapboxMap - ) - }, - onUpdate: { mapView in - Self.upsertRouteAlertsSymbolImages( - map: mapView.mapboxMap - ) - }, - onAfterRemove: { mapView in - do { - try Self.removeRouteAlertSymbolImages( - from: mapView.mapboxMap - ) - } catch { - Log.error( - "Failed to remove route alerts annotation images with error \(error)", - category: .navigationUI - ) - } - } - ), - ] - } - - private static let interpolationFactors = [ - 10.0: 1.0, - 14.5: 3.0, - 17.0: 6.0, - 22.0: 8.0, - ] - - private func roadObjectsFeatures( - for alerts: [RoadObjectAhead], - currentDistance: CLLocationDistance, - excludedRouteAlertTypes: RoadAlertType - ) -> [Feature] { - var features = [Feature]() - for alert in alerts where !alert.isExcluded(excludedRouteAlertTypes: excludedRouteAlertTypes) { - guard alert.distance == nil || alert.distance! >= currentDistance, - let objectInfo = info(for: alert.roadObject.kind) - else { continue } - let object = alert.roadObject - func addImage( - _ coordinate: LocationCoordinate2D, - _ distance: LocationDistance?, - color: UIColor? = nil - ) { - var feature = Feature(geometry: .point(.init(coordinate))) - let identifier: FeatureIdentifier = - .string("road-alert-\(coordinate.latitude)-\(coordinate.longitude)-\(features.count)") - let colorHex = (color ?? objectInfo.color ?? UIColor.gray).hexString - let properties: [String: JSONValue?] = [ - RoadObjectInfo.objectColor: JSONValue(rawValue: colorHex ?? UIColor.gray.hexString!), - RoadObjectInfo.objectImageType: .string(objectInfo.imageType.rawValue), - RoadObjectInfo.objectDistanceFromStart: .number(distance ?? 0.0), - RoadObjectInfo.distanceTraveled: .number(0.0), - ] - feature.properties = properties - feature.identifier = identifier - features.append(feature) - } - switch object.location { - case .routeAlert(shape: .lineString(let shape)): - guard - let startCoordinate = shape.coordinates.first, - let endCoordinate = shape.coordinates.last - else { - break - } - - if alert.distance.map({ $0 > 0 }) ?? true { - addImage(startCoordinate, alert.distance, color: .blue) - } - addImage(endCoordinate, alert.distance.map { $0 + (object.length ?? 0) }, color: .red) - case .routeAlert(shape: .point(let point)): - addImage(point.coordinates, alert.distance, color: nil) - case .openLRPoint(position: _, sideOfRoad: _, orientation: _, coordinate: let coordinates): - addImage(coordinates, alert.distance, color: nil) - case .openLRLine(path: _, shape: let geometry): - guard - let shape = openLRShape(from: geometry), - let startCoordinate = shape.coordinates.first, - let endCoordinate = shape.coordinates.last - else { - break - } - if alert.distance.map({ $0 > 0 }) ?? true { - addImage(startCoordinate, alert.distance, color: .blue) - } - addImage(endCoordinate, alert.distance.map { $0 + (object.length ?? 0) }, color: .red) - case .subgraph(enters: let enters, exits: let exits, shape: _, edges: _): - for enter in enters { - addImage(enter.coordinate, nil, color: .blue) - } - for exit in exits { - addImage(exit.coordinate, nil, color: .red) - } - default: - Log.error( - "Unexpected road object as Route Alert: \(object.identifier):\(object.kind)", - category: .navigationUI - ) - } - } - return features - } - - private func openLRShape(from geometry: Geometry) -> LineString? { - switch geometry { - case .point(let point): - return .init([point.coordinates]) - case .lineString(let lineString): - return lineString - default: - break - } - return nil - } - - private func info(for objectKind: RoadObject.Kind) -> RoadObjectInfo? { - switch objectKind { - case .incident(let incident): - let text = incident?.description - let color = incident?.impact.map(color(for:)) - switch incident?.kind { - case .congestion: - return .init(.congestion, text: text, color: color) - case .construction: - return .init(.construction, text: text, color: color) - case .roadClosure: - return .init(.roadClosure, text: text, color: color) - case .accident: - return .init(.accident, text: text, color: color) - case .disabledVehicle: - return .init(.disabledVehicle, text: text, color: color) - case .laneRestriction: - return .init(.laneRestriction, text: text, color: color) - case .massTransit: - return .init(.massTransit, text: text, color: color) - case .miscellaneous: - return .init(.miscellaneous, text: text, color: color) - case .otherNews: - return .init(.otherNews, text: text, color: color) - case .plannedEvent: - return .init(.plannedEvent, text: text, color: color) - case .roadHazard: - return .init(.roadHazard, text: text, color: color) - case .weather: - return .init(.weather, text: text, color: color) - case .undefined, .none: - return nil - } - default: - // We only show incidents on the map - return nil - } - } - - private func color(for impact: Incident.Impact) -> UIColor { - switch impact { - case .critical: - return .red - case .major: - return .purple - case .minor: - return .orange - case .low: - return .blue - case .unknown: - return .gray - } - } - - private static func upsertRouteAlertsSymbolImages( - map: MapboxMap - ) { - for (imageName, imageIdentifier) in imageNameToMapIdentifier(ids: RoadObjectFeature.ImageType.allCases) { - if let image = Bundle.module.image(named: imageName) { - map.provisionImage(id: imageIdentifier) { _ in - try map.addImage(image, id: imageIdentifier) - } - } else { - assertionFailure("No image for route alert \(imageName) in the bundle.") - } - } - } - - private static func removeRouteAlertSymbolImages( - from map: MapboxMap - ) throws { - for (_, imageIdentifier) in imageNameToMapIdentifier(ids: RoadObjectFeature.ImageType.allCases) { - try map.removeImage(withId: imageIdentifier) - } - } - - private static func imageNameToMapIdentifier( - ids: [RoadObjectFeature.ImageType] - ) -> [String: String] { - return ids.reduce(into: [String: String]()) { partialResult, type in - partialResult[type.imageName] = type.rawValue - } - } - - private struct RoadObjectFeature: Equatable { - enum ImageType: String, CaseIterable { - case accident - case congestion - case construction - case disabledVehicle = "disabled_vehicle" - case laneRestriction = "lane_restriction" - case massTransit = "mass_transit" - case miscellaneous - case otherNews = "other_news" - case plannedEvent = "planned_event" - case roadClosure = "road_closure" - case roadHazard = "road_hazard" - case weather - - var imageName: String { - switch self { - case .accident: - return "ra_accident" - case .congestion: - return "ra_congestion" - case .construction: - return "ra_construction" - case .disabledVehicle: - return "ra_disabled_vehicle" - case .laneRestriction: - return "ra_lane_restriction" - case .massTransit: - return "ra_mass_transit" - case .miscellaneous: - return "ra_miscellaneous" - case .otherNews: - return "ra_other_news" - case .plannedEvent: - return "ra_planned_event" - case .roadClosure: - return "ra_road_closure" - case .roadHazard: - return "ra_road_hazard" - case .weather: - return "ra_weather" - } - } - } - - struct Image: Equatable { - var id: String? - var type: ImageType - var coordinate: LocationCoordinate2D - var color: UIColor? - var text: String? - var isOnMainRoute: Bool - } - - struct Shape: Equatable { - var geometry: Geometry - } - - var id: String - var images: [Image] - var shape: Shape? - } - - private struct RoadObjectInfo { - var imageType: RoadObjectFeature.ImageType - var text: String? - var color: UIColor? - - init(_ imageType: RoadObjectFeature.ImageType, text: String? = nil, color: UIColor? = nil) { - self.imageType = imageType - self.text = text - self.color = color - } - - static let objectColor = "objectColor" - static let objectImageType = "objectImageType" - static let objectDistanceFromStart = "objectDistanceFromStart" - static let distanceTraveled = "distanceTraveled" - } -} - -extension RoadObjectAhead { - fileprivate func isExcluded(excludedRouteAlertTypes: RoadAlertType) -> Bool { - guard let roadAlertType = RoadAlertType(roadObjectKind: roadObject.kind) else { - return false - } - - return excludedRouteAlertTypes.contains(roadAlertType) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift deleted file mode 100644 index 90c7a55bd..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift +++ /dev/null @@ -1,55 +0,0 @@ -import _MapboxNavigationHelpers -import CoreLocation -import MapboxDirections -import MapboxMaps -import Turf -import UIKit - -/// Describes the possible annotation types on the route line. -public enum RouteAnnotationKind { - /// Shows the route duration. - case routeDurations - /// Shows the relative diff between the main route and the alternative. - /// The annotation is displayed in the approximate middle of the alternative steps. - case relativeDurationsOnAlternative - /// Shows the relative diff between the main route and the alternative. - /// The annotation is displayed next to the first different maneuver of the alternative road. - case relativeDurationsOnAlternativeManuever -} - -extension NavigationRoutes { - func routeDurationMapFeatures( - annotationKinds: Set, - config: MapStyleConfig - ) -> [any MapFeature] { - var showMainRoute = false - var showAlternatives = false - var showAsRelative = false - var annotateManeuver = false - for annotationKind in annotationKinds { - switch annotationKind { - case .routeDurations: - showMainRoute = true - showAlternatives = config.showsAlternatives - case .relativeDurationsOnAlternative: - showAsRelative = true - showAlternatives = config.showsAlternatives - case .relativeDurationsOnAlternativeManuever: - showAsRelative = true - annotateManeuver = true - showAlternatives = config.showsAlternatives - } - } - - return [ - ETAViewsAnnotationFeature( - for: self, - showMainRoute: showMainRoute, - showAlternatives: showAlternatives, - isRelative: showAsRelative, - annotateAtManeuver: annotateManeuver, - mapStyleConfig: config - ), - ] - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift deleted file mode 100644 index 66215f6f4..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift +++ /dev/null @@ -1,406 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxDirections -@_spi(Experimental) import MapboxMaps -import Turf -import UIKit - -struct LineGradientSettings { - let isSoft: Bool - let baseColor: UIColor - let featureColor: (Turf.Feature) -> UIColor -} - -struct RouteLineFeatureProvider { - var customRouteLineLayer: (String, String) -> Layer? - var customRouteCasingLineLayer: (String, String) -> Layer? - var customRouteRestrictedAreasLineLayer: (String, String) -> Layer? -} - -extension Route { - func routeLineMapFeatures( - ids: FeatureIds.RouteLine, - offset: Double, - isSoftGradient: Bool, - isAlternative: Bool, - config: MapStyleConfig, - featureProvider: RouteLineFeatureProvider, - customizedLayerProvider: CustomizedLayerProvider - ) -> [any MapFeature] { - var features: [any MapFeature] = [] - - if let shape { - let congestionFeatures = congestionFeatures( - legIndex: nil, - rangesConfiguration: config.congestionConfiguration.ranges - ) - let gradientStops = routeLineCongestionGradient( - congestionFeatures: congestionFeatures, - isMain: !isAlternative, - isSoft: isSoftGradient, - config: config - ) - let colors = config.congestionConfiguration.colors - let trafficGradient: Value = .expression( - .routeLineGradientExpression( - gradientStops, - lineBaseColor: isAlternative ? colors.alternativeRouteColors.unknown : colors.mainRouteColors - .unknown, - isSoft: isSoftGradient - ) - ) - - var sources: [GeoJsonMapFeature.Source] = [ - .init( - id: ids.source, - geoJson: .init(Feature(geometry: .lineString(shape))) - ), - ] - - let customRouteLineLayer = featureProvider.customRouteLineLayer(ids.main, ids.source) - let customRouteCasingLineLayer = featureProvider.customRouteCasingLineLayer(ids.casing, ids.source) - var layers: [any Layer] = [ - customRouteLineLayer ?? customizedLayerProvider.customizedLayer(defaultRouteLineLayer( - ids: ids, - isAlternative: isAlternative, - trafficGradient: trafficGradient, - config: config - )), - customRouteCasingLineLayer ?? customizedLayerProvider.customizedLayer(defaultRouteCasingLineLayer( - ids: ids, - isAlternative: isAlternative, - config: config - )), - ] - - if let traversedRouteColor = config.traversedRouteColor, !isAlternative, config.routeLineTracksTraversal { - layers.append( - customizedLayerProvider.customizedLayer(defaultTraversedRouteLineLayer( - ids: ids, - traversedRouteColor: traversedRouteColor, - config: config - )) - ) - } - - let restrictedRoadsFeatures: [Feature]? = config.isRestrictedAreaEnabled ? restrictedRoadsFeatures() : nil - let restrictedAreaGradientExpression: Value? = restrictedRoadsFeatures - .map { routeLineRestrictionsGradient($0, config: config) } - .map { - .expression( - MapboxMaps.Expression.routeLineGradientExpression( - $0, - lineBaseColor: config.routeRestrictedAreaColor - ) - ) - } - - if let restrictedRoadsFeatures, let restrictedAreaGradientExpression { - let shape = LineString(restrictedRoadsFeatures.compactMap { - guard case .lineString(let lineString) = $0.geometry else { - return nil - } - return lineString.coordinates - }.reduce([CLLocationCoordinate2D](), +)) - - sources.append( - .init( - id: ids.restrictedAreaSource, - geoJson: .geometry(.lineString(shape)) - ) - ) - let customRouteRestrictedAreasLine = featureProvider.customRouteRestrictedAreasLineLayer( - ids.restrictedArea, - ids.restrictedAreaSource - ) - - layers.append( - customRouteRestrictedAreasLine ?? - customizedLayerProvider.customizedLayer(defaultRouteRestrictedAreasLine( - ids: ids, - gradientExpression: restrictedAreaGradientExpression, - config: config - )) - ) - } - - features.append( - GeoJsonMapFeature( - id: ids.main, - sources: sources, - customizeSource: { source, _ in - source.lineMetrics = true - source.tolerance = 0.375 - }, - layers: layers, - onAfterAdd: { mapView in - mapView.mapboxMap.setRouteLineOffset(offset, for: ids) - }, - onUpdate: { mapView in - mapView.mapboxMap.setRouteLineOffset(offset, for: ids) - }, - onAfterUpdate: { mapView in - let map: MapboxMap = mapView.mapboxMap - try map.updateLayer(withId: ids.main, type: LineLayer.self, update: { layer in - layer.lineGradient = trafficGradient - }) - if let restrictedAreaGradientExpression { - try map.updateLayer(withId: ids.restrictedArea, type: LineLayer.self, update: { layer in - layer.lineGradient = restrictedAreaGradientExpression - }) - } - } - ) - ) - } - - return features - } - - private func defaultRouteLineLayer( - ids: FeatureIds.RouteLine, - isAlternative: Bool, - trafficGradient: Value, - config: MapStyleConfig - ) -> LineLayer { - let colors = config.congestionConfiguration.colors - let routeColors = isAlternative ? colors.alternativeRouteColors : colors.mainRouteColors - return with(LineLayer(id: ids.main, source: ids.source)) { - $0.lineColor = .constant(.init(routeColors.unknown)) - $0.lineWidth = .expression(.routeLineWidthExpression()) - $0.lineJoin = .constant(.round) - $0.lineCap = .constant(.round) - $0.lineGradient = trafficGradient - $0.lineDepthOcclusionFactor = config.occlusionFactor - $0.lineEmissiveStrength = .constant(1) - } - } - - private func defaultRouteCasingLineLayer( - ids: FeatureIds.RouteLine, - isAlternative: Bool, - config: MapStyleConfig - ) -> LineLayer { - let lineColor = isAlternative ? config.routeAlternateCasingColor : config.routeCasingColor - return with(LineLayer(id: ids.casing, source: ids.source)) { - $0.lineColor = .constant(.init(lineColor)) - $0.lineWidth = .expression(.routeCasingLineWidthExpression()) - $0.lineJoin = .constant(.round) - $0.lineCap = .constant(.round) - $0.lineDepthOcclusionFactor = config.occlusionFactor - $0.lineEmissiveStrength = .constant(1) - } - } - - private func defaultTraversedRouteLineLayer( - ids: FeatureIds.RouteLine, - traversedRouteColor: UIColor, - config: MapStyleConfig - ) -> LineLayer { - return with(LineLayer(id: ids.traversedRoute, source: ids.source)) { - $0.lineColor = .constant(.init(traversedRouteColor)) - $0.lineWidth = .expression(.routeLineWidthExpression()) - $0.lineJoin = .constant(.round) - $0.lineCap = .constant(.round) - $0.lineDepthOcclusionFactor = config.occlusionFactor - $0.lineEmissiveStrength = .constant(1) - } - } - - private func defaultRouteRestrictedAreasLine( - ids: FeatureIds.RouteLine, - gradientExpression: Value?, - config: MapStyleConfig - ) -> LineLayer { - return with(LineLayer(id: ids.restrictedArea, source: ids.restrictedAreaSource)) { - $0.lineColor = .constant(.init(config.routeRestrictedAreaColor)) - $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.5)) - $0.lineJoin = .constant(.round) - $0.lineCap = .constant(.round) - $0.lineOpacity = .constant(0.5) - $0.lineDepthOcclusionFactor = config.occlusionFactor - - $0.lineGradient = gradientExpression - $0.lineDasharray = .constant([0.5, 2.0]) - } - } - - func routeLineCongestionGradient( - congestionFeatures: [Turf.Feature]? = nil, - isMain: Bool = true, - isSoft: Bool, - config: MapStyleConfig - ) -> [Double: UIColor] { - // If `congestionFeatures` is set to nil - check if overridden route line casing is used. - let colors = config.congestionConfiguration.colors - let baseColor: UIColor = if let _ = congestionFeatures { - isMain ? colors.mainRouteColors.unknown : colors.alternativeRouteColors.unknown - } else { - config.routeCasingColor - } - let configuration = config.congestionConfiguration.colors - - let lineSettings = LineGradientSettings( - isSoft: isSoft, - baseColor: baseColor, - featureColor: { - guard config.showsTrafficOnRouteLine else { - return baseColor - } - if case .boolean(let isCurrentLeg) = $0.properties?[CurrentLegAttribute], isCurrentLeg { - let colors = isMain ? configuration.mainRouteColors : configuration.alternativeRouteColors - if case .string(let congestionLevel) = $0.properties?[CongestionAttribute] { - return congestionColor(for: congestionLevel, with: colors) - } else { - return congestionColor(for: nil, with: colors) - } - } - - return config.routeCasingColor - } - ) - - return routeLineFeaturesGradient(congestionFeatures, lineSettings: lineSettings) - } - - /// Given a congestion level, return its associated color. - func congestionColor(for congestionLevel: String?, with colors: CongestionColorsConfiguration.Colors) -> UIColor { - switch congestionLevel { - case "low": - return colors.low - case "moderate": - return colors.moderate - case "heavy": - return colors.heavy - case "severe": - return colors.severe - default: - return colors.unknown - } - } - - func routeLineFeaturesGradient( - _ routeLineFeatures: [Turf.Feature]? = nil, - lineSettings: LineGradientSettings - ) -> [Double: UIColor] { - var gradientStops = [Double: UIColor]() - var distanceTraveled = 0.0 - - if let routeLineFeatures { - let routeDistance = routeLineFeatures.compactMap { feature -> LocationDistance? in - if case .lineString(let lineString) = feature.geometry { - return lineString.distance() - } else { - return nil - } - }.reduce(0, +) - // lastRecordSegment records the last segmentEndPercentTraveled and associated congestion color added to the - // gradientStops. - var lastRecordSegment: (Double, UIColor) = (0.0, .clear) - - for (index, feature) in routeLineFeatures.enumerated() { - let associatedFeatureColor = lineSettings.featureColor(feature) - - guard case .lineString(let lineString) = feature.geometry, - let distance = lineString.distance() - else { - if gradientStops.isEmpty { - gradientStops[0.0] = lineSettings.baseColor - } - return gradientStops - } - let minimumPercentGap = 2e-16 - let stopGap = (routeDistance > 0.0) ? max( - min(GradientCongestionFadingDistance, distance * 0.1) / routeDistance, - minimumPercentGap - ) : minimumPercentGap - - if index == routeLineFeatures.startIndex { - distanceTraveled = distanceTraveled + distance - gradientStops[0.0] = associatedFeatureColor - - if index + 1 < routeLineFeatures.count { - let segmentEndPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 - var currentGradientStop = lineSettings - .isSoft ? segmentEndPercentTraveled - stopGap : - Double(CGFloat(segmentEndPercentTraveled).nextDown) - currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) - gradientStops[currentGradientStop] = associatedFeatureColor - lastRecordSegment = (currentGradientStop, associatedFeatureColor) - } - - continue - } - - if index == routeLineFeatures.endIndex - 1 { - if associatedFeatureColor == lastRecordSegment.1 { - gradientStops[lastRecordSegment.0] = nil - } else { - let segmentStartPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 - var currentGradientStop = lineSettings - .isSoft ? segmentStartPercentTraveled + stopGap : - Double(CGFloat(segmentStartPercentTraveled).nextUp) - currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) - gradientStops[currentGradientStop] = associatedFeatureColor - } - - continue - } - - if associatedFeatureColor == lastRecordSegment.1 { - gradientStops[lastRecordSegment.0] = nil - } else { - let segmentStartPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 - var currentGradientStop = lineSettings - .isSoft ? segmentStartPercentTraveled + stopGap : - Double(CGFloat(segmentStartPercentTraveled).nextUp) - currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) - gradientStops[currentGradientStop] = associatedFeatureColor - } - - distanceTraveled = distanceTraveled + distance - let segmentEndPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 - var currentGradientStop = lineSettings - .isSoft ? segmentEndPercentTraveled - stopGap : Double(CGFloat(segmentEndPercentTraveled).nextDown) - currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) - gradientStops[currentGradientStop] = associatedFeatureColor - lastRecordSegment = (currentGradientStop, associatedFeatureColor) - } - - if gradientStops.isEmpty { - gradientStops[0.0] = lineSettings.baseColor - } - - } else { - gradientStops[0.0] = lineSettings.baseColor - } - - return gradientStops - } - - func routeLineRestrictionsGradient( - _ restrictionFeatures: [Turf.Feature], - config: MapStyleConfig - ) -> [Double: UIColor] { - // If there's no restricted feature, hide the restricted route line layer. - guard restrictionFeatures.count > 0 else { - let gradientStops: [Double: UIColor] = [0.0: .clear] - return gradientStops - } - - let lineSettings = LineGradientSettings( - isSoft: false, - baseColor: config.routeRestrictedAreaColor, - featureColor: { - if case .boolean(let isRestricted) = $0.properties?[RestrictedRoadClassAttribute], - isRestricted - { - return config.routeRestrictedAreaColor - } - - return .clear // forcing hiding non-restricted areas - } - ) - - return routeLineFeaturesGradient(restrictionFeatures, lineSettings: lineSettings) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift deleted file mode 100644 index 42f86f6a3..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift +++ /dev/null @@ -1,66 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxDirections -import MapboxMaps -import Turf - -extension Route { - func voiceInstructionMapFeatures( - ids: FeatureIds.VoiceInstruction, - customizedLayerProvider: CustomizedLayerProvider - ) -> [any MapFeature] { - var featureCollection = FeatureCollection(features: []) - - for (legIndex, leg) in legs.enumerated() { - for (stepIndex, step) in leg.steps.enumerated() { - guard let instructions = step.instructionsSpokenAlongStep else { continue } - for instruction in instructions { - guard let shape = legs[legIndex].steps[stepIndex].shape, - let coordinateFromStart = LineString(shape.coordinates.reversed()) - .coordinateFromStart(distance: instruction.distanceAlongStep) else { continue } - - var feature = Feature(geometry: .point(Point(coordinateFromStart))) - feature.properties = [ - "instruction": .string(instruction.text), - ] - featureCollection.features.append(feature) - } - } - } - - let layers: [any Layer] = [ - with(SymbolLayer(id: ids.layer, source: ids.source)) { - let instruction = Exp(.toString) { - Exp(.get) { - "instruction" - } - } - - $0.textField = .expression(instruction) - $0.textSize = .constant(14) - $0.textHaloWidth = .constant(1) - $0.textHaloColor = .constant(.init(.white)) - $0.textOpacity = .constant(0.75) - $0.textAnchor = .constant(.bottom) - $0.textJustify = .constant(.left) - }, - with(CircleLayer(id: ids.circleLayer, source: ids.source)) { - $0.circleRadius = .constant(5) - $0.circleOpacity = .constant(0.75) - $0.circleColor = .constant(.init(.white)) - }, - ] - return [ - GeoJsonMapFeature( - id: ids.source, - sources: [ - .init( - id: ids.source, - geoJson: .featureCollection(featureCollection) - ), - ], - customizeSource: { _, _ in }, - layers: layers.map { customizedLayerProvider.customizedLayer($0) } - ), - ] - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift b/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift deleted file mode 100644 index 202043cf2..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift +++ /dev/null @@ -1,156 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxDirections -import MapboxMaps -import Turf -import UIKit - -struct WaypointFeatureProvider { - var customFeatures: ([Waypoint], Int) -> FeatureCollection? - var customCirleLayer: (String, String) -> CircleLayer? - var customSymbolLayer: (String, String) -> SymbolLayer? -} - -@MainActor -extension Route { - /// Generates a map feature that visually represents waypoints along a route line. - /// The waypoints include the start, destination, and any intermediate waypoints. - /// - Important: Only intermediate waypoints are marked with pins. The starting point and destination are excluded - /// from this. - func waypointsMapFeature( - mapView: MapView, - legIndex: Int, - config: MapStyleConfig, - featureProvider: WaypointFeatureProvider, - customizedLayerProvider: CustomizedLayerProvider - ) -> MapFeature? { - guard let startWaypoint = legs.first?.source else { return nil } - guard let destinationWaypoint = legs.last?.destination else { return nil } - - let intermediateWaypoints = config.showsIntermediateWaypoints - ? legs.dropLast().compactMap(\.destination) - : [] - let waypoints = [startWaypoint] + intermediateWaypoints + [destinationWaypoint] - - registerIntermediateWaypointImage(in: mapView) - - let customFeatures = featureProvider.customFeatures(waypoints, legIndex) - - return waypointsMapFeature( - with: customFeatures ?? waypointsFeatures(legIndex: legIndex, waypoints: waypoints), - config: config, - featureProvider: featureProvider, - customizedLayerProvider: customizedLayerProvider - ) - } - - private func waypointsFeatures(legIndex: Int, waypoints: [Waypoint]) -> FeatureCollection { - FeatureCollection( - features: waypoints.enumerated().map { waypointIndex, waypoint in - var feature = Feature(geometry: .point(Point(waypoint.coordinate))) - var properties: [String: JSONValue] = [:] - properties["waypointCompleted"] = .boolean(waypointIndex <= legIndex) - properties["waipointIconImage"] = waypointIndex > 0 && waypointIndex < waypoints.count - 1 - ? .string(NavigationMapView.ImageIdentifier.midpointMarkerImage) - : nil - feature.properties = properties - - return feature - } - ) - } - - private func registerIntermediateWaypointImage(in mapView: MapView) { - let intermediateWaypointImageId = NavigationMapView.ImageIdentifier.midpointMarkerImage - mapView.mapboxMap.provisionImage(id: intermediateWaypointImageId) { - try $0.addImage( - UIImage.midpointMarkerImage, - id: intermediateWaypointImageId, - stretchX: [], - stretchY: [] - ) - } - } - - private func waypointsMapFeature( - with features: FeatureCollection, - config: MapStyleConfig, - featureProvider: WaypointFeatureProvider, - customizedLayerProvider: CustomizedLayerProvider - ) -> MapFeature { - let circleLayer = featureProvider.customCirleLayer( - FeatureIds.RouteWaypoints.default.innerCircle, - FeatureIds.RouteWaypoints.default.source - ) ?? customizedLayerProvider.customizedLayer(defaultCircleLayer(config: config)) - - let symbolLayer = featureProvider.customSymbolLayer( - FeatureIds.RouteWaypoints.default.markerIcon, - FeatureIds.RouteWaypoints.default.source - ) ?? customizedLayerProvider.customizedLayer(defaultSymbolLayer) - - return GeoJsonMapFeature( - id: FeatureIds.RouteWaypoints.default.featureId, - sources: [ - .init( - id: FeatureIds.RouteWaypoints.default.source, - geoJson: .featureCollection(features) - ), - ], - customizeSource: { _, _ in }, - layers: [circleLayer, symbolLayer], - onBeforeAdd: { _ in }, - onAfterRemove: { _ in } - ) - } - - private func defaultCircleLayer(config: MapStyleConfig) -> CircleLayer { - with( - CircleLayer( - id: FeatureIds.RouteWaypoints.default.innerCircle, - source: FeatureIds.RouteWaypoints.default.source - ) - ) { - let opacity = Exp(.switchCase) { - Exp(.any) { - Exp(.get) { - "waypointCompleted" - } - } - 0 - 1 - } - - $0.circleColor = .constant(.init(config.waypointColor)) - $0.circleOpacity = .expression(opacity) - $0.circleEmissiveStrength = .constant(1) - $0.circleRadius = .expression(.routeCasingLineWidthExpression(0.5)) - $0.circleStrokeColor = .constant(.init(config.waypointStrokeColor)) - $0.circleStrokeWidth = .expression(.routeCasingLineWidthExpression(0.14)) - $0.circleStrokeOpacity = .expression(opacity) - $0.circlePitchAlignment = .constant(.map) - } - } - - private var defaultSymbolLayer: SymbolLayer { - with( - SymbolLayer( - id: FeatureIds.RouteWaypoints.default.markerIcon, - source: FeatureIds.RouteWaypoints.default.source - ) - ) { - let opacity = Exp(.switchCase) { - Exp(.any) { - Exp(.get) { - "waypointCompleted" - } - } - 0 - 1 - } - $0.iconOpacity = .expression(opacity) - $0.iconImage = .expression(Exp(.get) { "waipointIconImage" }) - $0.iconAnchor = .constant(.bottom) - $0.iconOffset = .constant([0, 15]) - $0.iconAllowOverlap = .constant(true) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/ElectronicHorizonController.swift b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/ElectronicHorizonController.swift deleted file mode 100644 index c2af65093..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/ElectronicHorizonController.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Combine -import Foundation - -/// Provides access to ElectronicHorizon events. -@MainActor -public protocol ElectronicHorizonController: Sendable { - /// Posts updates on EH. - var eHorizonEvents: AnyPublisher { get } - /// Provides access to the road graph network and related road objects. - var roadMatching: RoadMatching { get } - - /// Toggles ON EH updates. - /// - /// Requires ``ElectronicHorizonConfig`` to be provided. - func startUpdatingEHorizon() - /// Toggles OFF EH updates. - /// - /// Requires ``ElectronicHorizonConfig`` to be provided. - func stopUpdatingEHorizon() -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/MapboxNavigation.swift b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/MapboxNavigation.swift deleted file mode 100644 index 6f3a08f39..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/MapboxNavigation.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Combine -import Foundation - -/// An entry point for interacting with the Mapbox Navigation SDK. -@MainActor -public protocol MapboxNavigation { - /// Returns a ``RoutingProvider`` used by SDK - func routingProvider() -> RoutingProvider - - /// Provides control over main navigation states and transitions between them. - func tripSession() -> SessionController - - // TODO: add replaying controls - - /// Provides access to ElectronicHorizon events. - func electronicHorizon() -> ElectronicHorizonController - - /// Provides control over various aspects of the navigation process, mainly Active Guidance. - func navigation() -> NavigationController - - /// Provides access to observing and posting various navigation events and user feedback. - func eventsManager() -> NavigationEventsManager - - /// Provides ability to push custom history events to the log. - func historyRecorder() -> HistoryRecording? - - /// Provides access to the copilot service. - /// - /// Use this to get fine details of the current navigation session and manually control it. - func copilot() -> CopilotService? -} - -extension MapboxNavigator: - SessionController, - ElectronicHorizonController, - NavigationController -{ - public var locationMatching: AnyPublisher { - mapMatching - .compactMap { $0 } - .eraseToAnyPublisher() - } - - var currentLocationMatching: MapMatchingState? { - currentMapMatching - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/NavigationController.swift b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/NavigationController.swift deleted file mode 100644 index 466552f3b..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/NavigationController.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Combine -import CoreLocation -import Foundation -import MapboxDirections - -/// Provides control over various aspects of the navigation process, mainly Active Guidance. -@MainActor -public protocol NavigationController: Sendable { - /// Programmatically switches between available continuous alternatvies - /// - parameter index: An index of an alternative in ``NavigationRoutes/alternativeRoutes`` - func selectAlternativeRoute(at index: Int) - /// Programmatically switches between available continuous alternatvies - /// - parameter routeId: ``AlternativeRoute/id-swift.property`` of an alternative - func selectAlternativeRoute(with routeId: RouteId) - /// Manually switches current route leg. - /// - parameter newLegIndex: A leg index to switch to. - func switchLeg(newLegIndex: Int) - /// Posts heading updates. - var heading: AnyPublisher { get } - - /// Posts map matching updates, including location, current speed and speed limits, road info and map matching - /// details. - /// - /// - Note: To receive map matching updates through subscribres, initiate a free drive session by - /// invoking ``SessionController/startFreeDrive()`` or start an active guidance session - /// by invoking ``SessionController/startActiveGuidance(with:startLegIndex:)``. - var locationMatching: AnyPublisher { get } - /// Includes current location, speed, road info and additional map matching details. - var currentLocationMatching: MapMatchingState? { get } - /// Posts current route progress updates. - /// - /// - Note: This functionality is limited to the active guidance mode. - var routeProgress: AnyPublisher { get } - /// Current route progress updates. - /// - /// - Note: This functionality is limited to the active guidance mode. - var currentRouteProgress: RouteProgressState? { get } - - /// Posts updates about Navigator going to switch it's tiles version. - var offlineFallbacks: AnyPublisher { get } - - /// Posts updates about upcoming voice instructions. - var voiceInstructions: AnyPublisher { get } - /// Posts updates about upcoming visual instructions. - var bannerInstructions: AnyPublisher { get } - - /// Posts updates about arriving to route waypoints. - var waypointsArrival: AnyPublisher { get } - /// Posts updates about rerouting events and progress. - var rerouting: AnyPublisher { get } - /// Posts updates about continuous alternatives changes during the trip. - var continuousAlternatives: AnyPublisher { get } - /// Posts updates about faster routes applied during the trip. - var fasterRoutes: AnyPublisher { get } - /// Posts updates about route refreshing process. - var routeRefreshing: AnyPublisher { get } - - /// Posts updates about navigation-related errors happen. - var errors: AnyPublisher { get } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/SessionController.swift b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/SessionController.swift deleted file mode 100644 index 1353393ea..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigation/SessionController.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Combine -import Foundation - -/// Provides control over main navigation states and transitions between them. -@MainActor -public protocol SessionController: Sendable { - /// Transitions (or resumes) to the Free Drive mode. - func startFreeDrive() - /// Pauses the Free Drive. - /// - /// Does nothing if not in the Free Drive mode. - func pauseFreeDrive() - /// Transitions to Idle state. - /// - /// No navigation actions are performed in this state. Location updates are no collected and not processed. - func setToIdle() - /// Starts ActiveNavigation with the given `navigationRoutes`. - /// - parameter navigationRoutes: A route to navigate. - /// - parameter startLegIndex: A leg index, to start with. Usually start from `0`. - func startActiveGuidance(with navigationRoutes: NavigationRoutes, startLegIndex: Int) - - /// Posts updates of the current session state. - var session: AnyPublisher { get } - /// The current session state. - @MainActor - var currentSession: Session { get } - - /// Posts updates about the ``NavigationRoutes`` which navigator follows. - var navigationRoutes: AnyPublisher { get } - /// Current `NavigationRoutes` the navigator is following - var currentNavigationRoutes: NavigationRoutes? { get } - - /// Explicitly attempts to stop location updates on the background. - /// - /// Call this method when app is going background mode and you want to stop user tracking. - /// Works only for Free Drive mode, when ``CoreConfig/disableBackgroundTrackingLocation`` configuration is enabled. - func disableTrackingBackgroundLocationIfNeeded() - - /// Resumes location tracking after restoring from background mode. - /// - /// Call this method on restoring to foreground if you want to continue user tracking. - /// Works only for Free Drive mode, when ``CoreConfig/disableBackgroundTrackingLocation` configuration is enabled. - func restoreTrackingLocationIfNeeded() -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigationProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigationProvider.swift deleted file mode 100644 index bc3fdb0ac..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/MapboxNavigationProvider.swift +++ /dev/null @@ -1,374 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import MapboxCommon -import MapboxCommon_Private -import MapboxNavigationNative -import MapboxNavigationNative_Private - -public final class MapboxNavigationProvider { - let multiplexLocationClient: MultiplexLocationClient - - public var skuTokenProvider: SkuTokenProvider { - billingHandler.skuTokenProvider() - } - - public var predictiveCacheManager: PredictiveCacheManager? { - coreConfig.predictiveCacheConfig.map { - PredictiveCacheManager( - predictiveCacheOptions: $0, - tileStore: coreConfig.tilestoreConfig.navigatorLocation.tileStore - ) - } - } - - private var _sharedRouteVoiceController: RouteVoiceController? - @MainActor - public var routeVoiceController: RouteVoiceController { - if let _sharedRouteVoiceController { - return _sharedRouteVoiceController - } else { - let routeVoiceController = RouteVoiceController( - routeProgressing: navigation().routeProgress, - rerouteStarted: navigation().rerouting - .filter { $0.event is ReroutingStatus.Events.FetchingRoute } - .map { _ in } - .eraseToAnyPublisher(), - fasterRouteSet: navigation().fasterRoutes - .filter { $0.event is FasterRoutesStatus.Events.Applied } - .map { _ in } - .eraseToAnyPublisher(), - speechSynthesizer: coreConfig.ttsConfig.speechSynthesizer( - with: coreConfig.locale, - apiConfiguration: coreConfig.credentials.speech, - skuTokenProvider: skuTokenProvider - ) - ) - _sharedRouteVoiceController = routeVoiceController - return routeVoiceController - } - } - - private let _coreConfig: UnfairLocked - - public var coreConfig: CoreConfig { - _coreConfig.read() - } - - /// Creates a new ``MapboxNavigationProvider``. - /// - /// You should never instantiate multiple instances of ``MapboxNavigationProvider`` simultaneously. - /// - parameter coreConfig: A configuration for the SDK. It is recommended not modify the configuration during - /// operation, but it is still possible via ``MapboxNavigationProvider/apply(coreConfig:)``. - public init(coreConfig: CoreConfig) { - Self.checkInstanceIsUnique() - self._coreConfig = .init(coreConfig) - self.multiplexLocationClient = MultiplexLocationClient(source: coreConfig.locationSource) - apply(coreConfig: coreConfig) - SdkInfoRegistryFactory.getInstance().registerSdkInformation(forInfo: SdkInfo.navigationCore.native) - MovementMonitorFactory.setUserDefinedForCustom(movementMonitor) - } - - /// Updates the SDK configuration. - /// - /// It is not recommended to do so due to some updates may be propagated incorrectly. - /// - Parameter coreConfig: The configuration for the SDK. - public func apply(coreConfig: CoreConfig) { - _coreConfig.update(coreConfig) - - let logLevel = NSNumber(value: coreConfig.logLevel.rawValue) - LogConfiguration.setLoggingLevelForCategory("nav-native", upTo: logLevel) - LogConfiguration.setLoggingLevelForUpTo(logLevel) - eventsMetadataProvider.userInfo = coreConfig.telemetryAppMetadata?.configuration - - MapboxOptions.accessToken = coreConfig.credentials.map.accessToken - let copilotEnabled = coreConfig.copilotEnabled - let locationSource = coreConfig.locationSource - let locationClient = multiplexLocationClient - let ttsConfig = coreConfig.ttsConfig - let locale = coreConfig.locale - let speechApiConfiguration = coreConfig.credentials.speech - let skuTokenProvider = skuTokenProvider - - nativeHandlersFactory.locale = coreConfig.locale - Task { @MainActor [_copilot, _sharedRouteVoiceController] in - await _copilot?.setActive(copilotEnabled) - if locationClient.isInitialized { - locationClient.setLocationSource(locationSource) - } - _sharedRouteVoiceController?.speechSynthesizer = ttsConfig.speechSynthesizer( - with: locale, - apiConfiguration: speechApiConfiguration, - skuTokenProvider: skuTokenProvider - ) - } - } - - /// Provides an entry point for interacting with the Mapbox Navigation SDK. - /// - /// This instance is shared. - @MainActor - public var mapboxNavigation: MapboxNavigation { - self - } - - /// Gets TilesetDescriptor that corresponds to the latest available version of routing tiles. - /// - /// It is intended to be used when creating off-line tile packs. - public func getLatestNavigationTilesetDescriptor() -> TilesetDescriptor { - TilesetDescriptorFactory.getLatestForCache(nativeHandlersFactory.cacheHandle) - } - - // MARK: - Instance Lifecycle control - - private static let hasInstance: NSLocked = .init(false) - - private static func checkInstanceIsUnique() { - hasInstance.mutate { hasInstance in - if hasInstance { - Log.fault( - "[BUG] Two simultaneous active navigation cores. Profile the app and make sure that MapboxNavigationProvider is allocated only once.", - category: .navigation - ) - preconditionFailure("MapboxNavigationProvider was instantiated twice.") - } - hasInstance = true - } - } - - private func unregisterUniqueInstance() { - Self.hasInstance.update(false) - } - - deinit { - unregisterUniqueInstance() - } - - // MARK: - Internal members - - private weak var _sharedNavigator: MapboxNavigator? - @MainActor - func navigator() -> MapboxNavigator { - if let sharedNavigator = _sharedNavigator { - return sharedNavigator - } else { - let coreNavigator: CoreNavigator = NativeNavigator( - with: .init( - credentials: coreConfig.credentials.navigation, - nativeHandlersFactory: nativeHandlersFactory, - routingConfig: coreConfig.routingConfig, - predictiveCacheManager: predictiveCacheManager - ) - ) - let fasterRouteController = coreConfig.routingConfig.fasterRouteDetectionConfig.map { - return $0.customFasterRouteProvider ?? FasterRouteController( - configuration: .init( - settings: $0, - initialManeuverAvoidanceRadius: coreConfig.routingConfig.initialManeuverAvoidanceRadius, - routingProvider: routingProvider() - ) - ) - } - - let newNavigator = MapboxNavigator( - configuration: .init( - navigator: coreNavigator, - routeParserType: RouteParser.self, - locationClient: multiplexLocationClient.locationClient, - alternativesAcceptionPolicy: coreConfig.routingConfig.alternativeRoutesDetectionConfig? - .acceptionPolicy, - billingHandler: billingHandler, - multilegAdvancing: coreConfig.multilegAdvancing, - prefersOnlineRoute: coreConfig.routingConfig.prefersOnlineRoute, - disableBackgroundTrackingLocation: coreConfig.disableBackgroundTrackingLocation, - fasterRouteController: fasterRouteController, - electronicHorizonConfig: coreConfig.electronicHorizonConfig, - congestionConfig: coreConfig.congestionConfig, - movementMonitor: movementMonitor - ) - ) - _sharedNavigator = newNavigator - _ = eventsManager() - - multiplexLocationClient.subscribeToNavigatorUpdates( - newNavigator, - source: coreConfig.locationSource - ) - - // Telemetry needs to be created for Navigator - - return newNavigator - } - } - - private var _billingHandler: UnfairLocked = .init(nil) - var billingHandler: BillingHandler { - _billingHandler.mutate { lazyInstance in - if let lazyInstance { - return lazyInstance - } else { - let newInstance = coreConfig.__customBillingHandler?() - ?? BillingHandler.createInstance(with: coreConfig.credentials.navigation.accessToken) - lazyInstance = newInstance - return newInstance - } - } - } - - lazy var nativeHandlersFactory: NativeHandlersFactory = .init( - tileStorePath: coreConfig.tilestoreConfig.navigatorLocation.tileStoreURL?.path ?? "", - apiConfiguration: coreConfig.credentials.navigation, - tilesVersion: coreConfig.tilesVersion, - targetVersion: nil, - configFactoryType: ConfigFactory.self, - datasetProfileIdentifier: coreConfig.routeRequestConfig.profileIdentifier, - routingProviderSource: coreConfig.routingConfig.routingProviderSource.nativeSource, - liveIncidentsOptions: coreConfig.liveIncidentsConfig, - navigatorPredictionInterval: coreConfig.navigatorPredictionInterval, - statusUpdatingSettings: nil, - utilizeSensorData: coreConfig.utilizeSensorData, - historyDirectoryURL: coreConfig.historyRecordingConfig?.historyDirectoryURL, - initialManeuverAvoidanceRadius: coreConfig.routingConfig.initialManeuverAvoidanceRadius, - locale: coreConfig.locale - ) - - private lazy var _historyRecorder: HistoryRecording? = { - guard let historyDirectoryURL = coreConfig.historyRecordingConfig?.historyDirectoryURL else { - return nil - } - do { - let fileManager = FileManager.default - try fileManager.createDirectory(at: historyDirectoryURL, withIntermediateDirectories: true, attributes: nil) - } catch { - Log.error( - "Failed to create history saving directory at '\(historyDirectoryURL)' due to error: \(error)", - category: .settings - ) - return nil - } - return nativeHandlersFactory.historyRecorderHandle.map { - HistoryRecorder(handle: $0) - } - }() - - private lazy var _copilot: CopilotService? = { - guard let _historyRecorder else { return nil } - let version = onMainQueueSync { nativeHandlersFactory.navigator.native.version() } - return .init( - accessToken: coreConfig.credentials.navigation.accessToken, - navNativeVersion: version, - historyRecording: _historyRecorder, - isActive: coreConfig.copilotEnabled, - log: { logOutput in - Log.debug( - "\(logOutput)", - category: .copilot - ) - } - ) - }() - - var eventsMetadataProvider: EventsMetadataProvider { - onMainQueueSync { - let eventsMetadataProvider = EventsMetadataProvider( - appState: EventAppState(), - screen: .main, - device: .current - ) - eventsMetadataProvider.userInfo = coreConfig.telemetryAppMetadata?.configuration - return eventsMetadataProvider - } - } - - // Need to store the metadata provider and NN Telemetry - private var _sharedEventsManager: UnfairLocked = .init(nil) - - var movementMonitor: NavigationMovementMonitor { - _sharedMovementMonitor.mutate { _sharedMovementMonitor in - if let _sharedMovementMonitor { - return _sharedMovementMonitor - } - let movementMonitor = NavigationMovementMonitor() - _sharedMovementMonitor = movementMonitor - return movementMonitor - } - } - - private var _sharedMovementMonitor: UnfairLocked = .init(nil) -} - -// MARK: - MapboxNavigation implementation - -extension MapboxNavigationProvider: MapboxNavigation { - public func routingProvider() -> RoutingProvider { - if let customProvider = coreConfig.__customRoutingProvider { - return customProvider() - } - return MapboxRoutingProvider( - with: .init( - source: coreConfig.routingConfig.routingProviderSource, - nativeHandlersFactory: nativeHandlersFactory, - credentials: .init(coreConfig.credentials.navigation) - ) - ) - } - - public func tripSession() -> SessionController { - navigator() - } - - public func electronicHorizon() -> ElectronicHorizonController { - navigator() - } - - public func navigation() -> NavigationController { - navigator() - } - - public func eventsManager() -> NavigationEventsManager { - let telemetry = nativeHandlersFactory - .telemetry(eventsMetadataProvider: eventsMetadataProvider) - return _sharedEventsManager.mutate { _sharedEventsManager in - if let _sharedEventsManager { - return _sharedEventsManager - } - let eventsMetadataProvider = eventsMetadataProvider - let eventsManager = coreConfig.__customEventsManager?() ?? NavigationEventsManager( - eventsMetadataProvider: eventsMetadataProvider, - telemetry: telemetry - ) - _sharedEventsManager = eventsManager - return eventsManager - } - } - - public func historyRecorder() -> HistoryRecording? { - _historyRecorder - } - - public func copilot() -> CopilotService? { - _copilot - } -} - -extension TTSConfig { - @MainActor - fileprivate func speechSynthesizer( - with locale: Locale, - apiConfiguration: ApiConfiguration, - skuTokenProvider: SkuTokenProvider - ) -> SpeechSynthesizing { - let speechSynthesizer = switch self { - case .default: - MultiplexedSpeechSynthesizer( - mapboxSpeechApiConfiguration: apiConfiguration, - skuTokenProvider: skuTokenProvider.skuToken - ) - case .localOnly: - MultiplexedSpeechSynthesizer(speechSynthesizers: [SystemSpeechSynthesizer()]) - case .custom(let speechSynthesizer): - speechSynthesizer - } - speechSynthesizer.locale = locale - return speechSynthesizer - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/AlternativeRoute.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/AlternativeRoute.swift deleted file mode 100644 index 499c3eb8a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/AlternativeRoute.swift +++ /dev/null @@ -1,175 +0,0 @@ -import Foundation -import MapboxDirections -import MapboxNavigationNative -import Turf - -/// Additional reasonable routes besides the main roure that visit waypoints. -public struct AlternativeRoute: @unchecked Sendable { - let nativeRoute: RouteInterface - var isForkPointPassed: Bool = false - - /// A `Route` object that the current alternative route represents. - public let route: Route - - /// Alternative route identifier type - public typealias ID = UInt32 - /// Brief statistics of a route for traveling - public struct RouteInfo { - /// Expected travel distance - public let distance: LocationDistance - /// Expected travel duration - public let duration: TimeInterval - - public init(distance: LocationDistance, duration: TimeInterval) { - self.distance = distance - self.duration = duration - } - } - - /// Holds related indices values of an intersection. - public struct IntersectionGeometryIndices { - /// The leg index within a route - public let legIndex: Int - /// The geometry index of an intersection within leg geometry - public let legGeometryIndex: Int - /// The geometry index of an intersection within route geometry - public let routeGeometryIndex: Int - } - - /// Alternative route identificator. - /// - /// It is unique within the same navigation session. - public let id: ID - /// Unique route id. - public let routeId: RouteId - /// Intersection on the main route, where alternative route branches. - public let mainRouteIntersection: Intersection - /// Indices values of an intersection on the main route - public let mainRouteIntersectionIndices: IntersectionGeometryIndices - /// Intersection on the alternative route, where it splits from the main route. - public let alternativeRouteIntersection: Intersection - /// Indices values of an intersection on the alternative route - public let alternativeRouteIntersectionIndices: IntersectionGeometryIndices - /// Alternative route statistics, counting from the split point. - public let infoFromDeviationPoint: RouteInfo - /// Alternative route statistics, counting from it's origin. - public let infoFromOrigin: RouteInfo - /// The difference of distances between alternative and the main routes - public let distanceDelta: LocationDistance - /// The difference of expected travel time between alternative and the main routes - public let expectedTravelTimeDelta: TimeInterval - - public init?(mainRoute: Route, alternativeRoute nativeRouteAlternative: RouteAlternative) async { - guard let route = try? await nativeRouteAlternative.route.convertToDirectionsRoute() else { - return nil - } - - self.init(mainRoute: mainRoute, alternativeRoute: route, nativeRouteAlternative: nativeRouteAlternative) - } - - init?(mainRoute: Route, alternativeRoute: Route, nativeRouteAlternative: RouteAlternative) { - self.nativeRoute = nativeRouteAlternative.route - self.route = alternativeRoute - - self.id = nativeRouteAlternative.id - self.routeId = .init(rawValue: nativeRouteAlternative.route.getRouteId()) - - var legIndex = Int(nativeRouteAlternative.mainRouteFork.legIndex) - var segmentIndex = Int(nativeRouteAlternative.mainRouteFork.segmentIndex) - - self.mainRouteIntersectionIndices = .init( - legIndex: legIndex, - legGeometryIndex: segmentIndex, - routeGeometryIndex: Int(nativeRouteAlternative.mainRouteFork.geometryIndex) - ) - - guard let mainIntersection = mainRoute.findIntersection(on: legIndex, by: segmentIndex) else { - return nil - } - self.mainRouteIntersection = mainIntersection - - legIndex = Int(nativeRouteAlternative.alternativeRouteFork.legIndex) - segmentIndex = Int(nativeRouteAlternative.alternativeRouteFork.segmentIndex) - self.alternativeRouteIntersectionIndices = .init( - legIndex: legIndex, - legGeometryIndex: segmentIndex, - routeGeometryIndex: Int(nativeRouteAlternative.alternativeRouteFork.geometryIndex) - ) - - guard let alternativeIntersection = alternativeRoute.findIntersection(on: legIndex, by: segmentIndex) else { - return nil - } - self.alternativeRouteIntersection = alternativeIntersection - - self.infoFromDeviationPoint = .init( - distance: nativeRouteAlternative.infoFromFork.distance, - duration: nativeRouteAlternative.infoFromFork.duration - ) - self.infoFromOrigin = .init( - distance: nativeRouteAlternative.infoFromStart.distance, - duration: nativeRouteAlternative.infoFromStart.duration - ) - - self.distanceDelta = infoFromOrigin.distance - mainRoute.distance - self.expectedTravelTimeDelta = infoFromOrigin.duration - mainRoute.expectedTravelTime - } - - static func fromNative( - alternativeRoutes: [RouteAlternative], - relateveTo mainRoute: NavigationRoute - ) async -> [AlternativeRoute] { - var converted = [AlternativeRoute?](repeating: nil, count: alternativeRoutes.count) - await withTaskGroup(of: (Int, AlternativeRoute?).self) { group in - for (index, alternativeRoute) in alternativeRoutes.enumerated() { - group.addTask { - let alternativeRoute = await AlternativeRoute( - mainRoute: mainRoute.route, - alternativeRoute: alternativeRoute - ) - return (index, alternativeRoute) - } - } - - for await (index, alternativeRoute) in group { - guard let alternativeRoute else { - Log.error( - "Alternative routes parsing lost route with id: \(alternativeRoutes[index].route.getRouteId())", - category: .navigation - ) - continue - } - converted[index] = alternativeRoute - } - } - - return converted.compactMap { $0 } - } -} - -extension AlternativeRoute: Equatable { - public static func == (lhs: AlternativeRoute, rhs: AlternativeRoute) -> Bool { - return lhs.routeId == rhs.routeId && - lhs.route == rhs.route - } -} - -extension Route { - fileprivate func findIntersection(on legIndex: Int, by segmentIndex: Int) -> Intersection? { - guard legs.count > legIndex else { - return nil - } - - var leg = legs[legIndex] - guard let stepindex = leg.segmentRangesByStep.firstIndex(where: { $0.contains(segmentIndex) }) else { - return nil - } - - guard let intersectionIndex = leg.steps[stepindex].segmentIndicesByIntersection? - .firstIndex(where: { $0 == segmentIndex }) - else { - return nil - } - - return leg.steps[stepindex].intersections?[intersectionIndex] - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/BorderCrossing.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/BorderCrossing.swift deleted file mode 100644 index 0576e40e3..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/BorderCrossing.swift +++ /dev/null @@ -1,32 +0,0 @@ - -import Foundation -import MapboxDirections -import MapboxNavigationNative - -extension AdministrativeRegion { - init(_ adminInfo: AdminInfo) { - self.init(countryCode: adminInfo.iso_3166_1, countryCodeAlpha3: adminInfo.iso_3166_1_alpha3) - } -} - -/// ``BorderCrossing`` encapsulates a border crossing, specifying crossing region codes. -public struct BorderCrossing: Equatable { - public let from: AdministrativeRegion - public let to: AdministrativeRegion - - /// Initializes a new ``BorderCrossing`` object. - /// - Parameters: - /// - from: origin administrative region - /// - to: destination administrative region - public init(from: AdministrativeRegion, to: AdministrativeRegion) { - self.from = from - self.to = to - } - - init(_ borderCrossing: BorderCrossingInfo) { - self.init( - from: AdministrativeRegion(borderCrossing.from), - to: AdministrativeRegion(borderCrossing.to) - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/DistancedRoadObject.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/DistancedRoadObject.swift deleted file mode 100644 index 4a76bc531..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/DistancedRoadObject.swift +++ /dev/null @@ -1,155 +0,0 @@ -import CoreLocation -import Foundation -import MapboxNavigationNative - -/// Contains information about distance to the road object of a concrete type/shape (gantry, polygon, line, point etc.). -public enum DistancedRoadObject: Sendable, Equatable { - /// The information about distance to the road object represented as a point. - /// - Parameters: - /// - identifier: Road object identifier. - /// - kind: Road object kind. - /// - distance: Distance to the point object, measured in meters. - case point( - identifier: RoadObject.Identifier, - kind: RoadObject.Kind, - distance: CLLocationDistance - ) - - /// The information about distance to the road object represented as a gantry. - /// - Parameters: - /// - identifier: Road object identifier. - /// - kind: Road object kind. - /// - distance: Distance to the gantry object. - case gantry( - identifier: RoadObject.Identifier, - kind: RoadObject.Kind, - distance: CLLocationDistance - ) - - /// The information about distance to the road object represented as a polygon. - /// - Parameters: - /// - identifier: Road object identifier. - /// - kind: Road object kind. - /// - distanceToNearestEntry: Distance measured in meters to the nearest entry. - /// - distanceToNearestExit: Distance measured in meters to nearest exit. - /// - isInside: Boolean to indicate whether we're currently "inside" the object. - case polygon( - identifier: RoadObject.Identifier, - kind: RoadObject.Kind, - distanceToNearestEntry: CLLocationDistance?, - distanceToNearestExit: CLLocationDistance?, - isInside: Bool - ) - - /// The information about distance to the road object represented as a subgraph. - /// - Parameters: - /// - identifier: Road object identifier. - /// - kind: Road object kind. - /// - distanceToNearestEntry: Distance measured in meters to the nearest entry. - /// - distanceToNearestExit: Distance measured in meters to the nearest exit. - /// - isInside: Boolean that indicates whether we're currently "inside" the object. - case subgraph( - identifier: RoadObject.Identifier, - kind: RoadObject.Kind, - distanceToNearestEntry: CLLocationDistance?, - distanceToNearestExit: CLLocationDistance?, - isInside: Bool - ) - - /// The information about distance to the road object represented as a line. - /// - Parameters: - /// - identifier: Road object identifier. - /// - kind: Road object kind. - /// - distanceToEntry: Distance from the current position to entry point measured in meters along the road - /// graph. This value is 0 if already "within" the object. - /// - distanceToExit: Distance from the current position to the most likely exit point measured in meters along - /// the road graph. - /// - distanceToEnd: Distance from the current position to the most distance exit point measured in meters along - /// the road graph. - /// - isEntryFromStart: Boolean that indicates whether we enter the road object from its start. This value is - /// `false` if already "within" the object. - /// - length: Length of the road object measured in meters. - case line( - identifier: RoadObject.Identifier, - kind: RoadObject.Kind, - distanceToEntry: CLLocationDistance, - distanceToExit: CLLocationDistance, - distanceToEnd: CLLocationDistance, - isEntryFromStart: Bool, - length: CLLocationDistance - ) - - /// Road object identifier. - public var identifier: RoadObject.Identifier { - switch self { - case .point(let identifier, _, _), - .gantry(let identifier, _, _), - .polygon(let identifier, _, _, _, _), - .subgraph(let identifier, _, _, _, _), - .line(let identifier, _, _, _, _, _, _): - return identifier - } - } - - /// Road object kind. - public var kind: RoadObject.Kind { - switch self { - case .point(_, let type, _), - .gantry(_, let type, _), - .polygon(_, let type, _, _, _), - .subgraph(_, let type, _, _, _), - .line(_, let type, _, _, _, _, _): - return type - } - } - - init(_ native: MapboxNavigationNative.RoadObjectDistance) { - switch native.distanceInfo.type { - case .pointDistanceInfo: - let info = native.distanceInfo.getPointDistanceInfo() - self = .point( - identifier: native.roadObjectId, - kind: RoadObject.Kind(native.type), - distance: info.distance - ) - case .gantryDistanceInfo: - let info = native.distanceInfo.getGantryDistanceInfo() - self = .gantry( - identifier: native.roadObjectId, - kind: RoadObject.Kind(native.type), - distance: info.distance - ) - case .polygonDistanceInfo: - let info = native.distanceInfo.getPolygonDistanceInfo() - self = .polygon( - identifier: native.roadObjectId, - kind: RoadObject.Kind(native.type), - distanceToNearestEntry: info.entrances.first?.distance, - distanceToNearestExit: info.exits.first?.distance, - isInside: info.inside - ) - case .subGraphDistanceInfo: - let info = native.distanceInfo.getSubGraphDistanceInfo() - self = .subgraph( - identifier: native.roadObjectId, - kind: RoadObject.Kind(native.type), - distanceToNearestEntry: info.entrances.first?.distance, - distanceToNearestExit: info.exits.first?.distance, - isInside: info.inside - ) - case .lineDistanceInfo: - let info = native.distanceInfo.getLineDistanceInfo() - self = .line( - identifier: native.roadObjectId, - kind: RoadObject.Kind(native.type), - distanceToEntry: info.distanceToEntry, - distanceToExit: info.distanceToExit, - distanceToEnd: info.distanceToEnd, - isEntryFromStart: info.entryFromStart, - length: info.length - ) - @unknown default: - preconditionFailure("DistancedRoadObject can't be constructed. Unknown type.") - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/ElectronicHorizonConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/ElectronicHorizonConfig.swift deleted file mode 100644 index 646a32135..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/ElectronicHorizonConfig.swift +++ /dev/null @@ -1,57 +0,0 @@ -import CoreLocation -import Foundation -import MapboxNavigationNative - -/// Defines options for emitting ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition``, -/// ``Foundation/NSNotification/Name/electronicHorizonDidEnterRoadObject``, and -/// ``Foundation/NSNotification/Name/electronicHorizonDidExitRoadObject`` notifications while active guidance or free -/// drive is in progress. -/// -/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to -/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms -/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require -/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the -/// feature. -public struct ElectronicHorizonConfig: Equatable, Sendable { - /// The minimum length of the electronic horizon ahead of the current position, measured in meters. - public let length: CLLocationDistance - - /// The number of levels of branches by which to expand the horizon. - /// - /// A value of 0 results in only the most probable path (MPP). A value of 1 adds paths branching out directly from - /// the MPP, a value of 2 adds paths branching out from those paths, and so on. Only 0, 1, and 2 are usable in terms - /// of performance. - public let expansionLevel: UInt - - /// Minimum length of side branches, measured in meters. - public let branchLength: CLLocationDistance - - /// Minimum time which should pass between consecutive navigation statuses to update electronic horizon (seconds). - /// If `nil` we update electronic horizon on each navigation status. - public let minimumTimeIntervalBetweenUpdates: TimeInterval? - - public init( - length: CLLocationDistance, - expansionLevel: UInt, - branchLength: CLLocationDistance, - minTimeDeltaBetweenUpdates: TimeInterval? - ) { - self.length = length - self.expansionLevel = expansionLevel - self.branchLength = branchLength - self.minimumTimeIntervalBetweenUpdates = minTimeDeltaBetweenUpdates - } -} - -extension MapboxNavigationNative.ElectronicHorizonOptions { - convenience init(_ options: ElectronicHorizonConfig) { - self.init( - length: options.length, - expansion: UInt8(options.expansionLevel), - branchLength: options.branchLength, - doNotRecalculateInUncertainState: true, - minTimeDeltaBetweenUpdates: options.minimumTimeIntervalBetweenUpdates as NSNumber?, - alertsService: nil - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Interchange.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Interchange.swift deleted file mode 100644 index 782109c14..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Interchange.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// Contains information about routing and passing interchange along the route. -public struct Interchange: Equatable { - /// Interchange identifier, if available. - public var identifier: String - /// The localized names of the interchange, if available. - public let names: [LocalizedRoadObjectName] - - /// Initializes a new `Interchange` object. - /// - Parameters: - /// - names: The localized names of the interchange. - public init(names: [LocalizedRoadObjectName]) { - self.identifier = "" - self.names = names - } - - /// Initializes a new `Interchange` object. - /// - Parameters: - /// - identifier: Interchange identifier. - /// - names: The localized names of the interchange. - public init(identifier: String, names: [LocalizedRoadObjectName]) { - self.identifier = identifier - self.names = names - } - - init(_ icInfo: IcInfo) { - let names = icInfo.name.map { LocalizedRoadObjectName($0) } - self.init(identifier: icInfo.id, names: names) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Junction.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Junction.swift deleted file mode 100644 index dbb36212b..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/Junction.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// Contains information about routing and passing junction along the route. -public struct Junction: Equatable { - /// Junction identifier, if available. - public var identifier: String - /// The localized names of the junction, if available. - public let names: [LocalizedRoadObjectName] - - /// Initializes a new `Junction` object. - /// - Parameters: - /// - names: The localized names of the interchange. - public init(names: [LocalizedRoadObjectName]) { - self.identifier = "" - self.names = names - } - - /// Initializes a new `Junction` object. - /// - Parameters: - /// - identifier: Junction identifier. - /// - names: The localized names of the interchange. - public init(identifier: String, names: [LocalizedRoadObjectName]) { - self.identifier = identifier - self.names = names - } - - init(_ jctInfo: JctInfo) { - let names = jctInfo.name.map { LocalizedRoadObjectName($0) } - self.init(identifier: jctInfo.id, names: names) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/LocalizedRoadObjectName.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/LocalizedRoadObjectName.swift deleted file mode 100644 index f076d1358..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/LocalizedRoadObjectName.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// Road object information, like interchange name. -public struct LocalizedRoadObjectName: Equatable { - /// 2 letters language code, e.g. en or ja. - public let language: String - - /// The name of the road object. - public let text: String - - /// Initializes a new `LocalizedRoadObjectName` object. - /// - Parameters: - /// - language: 2 letters language code, e.g. en or ja. - /// - text: The name of the road object. - public init(language: String, text: String) { - self.language = language - self.text = text - } - - init(_ localizedString: LocalizedString) { - self.init(language: localizedString.language, text: localizedString.value) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRIdentifier.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRIdentifier.swift deleted file mode 100644 index 31e859922..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRIdentifier.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// Identifies a road object according to one of two OpenLR standards. -/// -/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to -/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms -/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require -/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the -/// feature. -public enum OpenLRIdentifier { - /// [TomTom OpenLR](http://www.openlr.org/). - /// - /// Supported references: line location, point along line, polygon. - case tomTom(reference: RoadObject.Identifier) - - /// TPEG OpenLR. - /// - /// Only line locations are supported. - case tpeg(reference: RoadObject.Identifier) -} - -extension MapboxNavigationNative.Standard { - init(identifier: OpenLRIdentifier) { - switch identifier { - case .tomTom: - self = .tomTom - case .tpeg: - self = .TPEG - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLROrientation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLROrientation.swift deleted file mode 100644 index 3152ddf37..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLROrientation.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// Describes the relationship between the road object and the direction of a eferenced line. The road object may be -/// directed in the same direction as the line, against that direction, both directions, or the direction of the road -/// object might be unknown. -/// -/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to -/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms -/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require -/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the -/// feature. -public enum OpenLROrientation: Equatable, Sendable { - /// The relationship between the road object and the direction of the referenced line is unknown. - case unknown - /// The road object is directed in the same direction as the referenced line. - case alongLine - /// The road object is directed against the direction of the referenced line. - case againstLine - /// The road object is directed in both directions. - case both - - init(_ native: MapboxNavigationNative.Orientation) { - switch native { - case .noOrientationOrUnknown: - self = .unknown - case .withLineDirection: - self = .alongLine - case .againstLineDirection: - self = .againstLine - case .both: - self = .both - @unknown default: - assertionFailure("Unknown OpenLROrientation type.") - self = .unknown - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRSideOfRoad.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRSideOfRoad.swift deleted file mode 100644 index 6174bdaa1..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/OpenLRSideOfRoad.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// Describes the relationship between the road object and the road. -/// The road object can be on the right side of the road, on the left side of the road, on both sides of the road or -/// directly on the road. -/// -/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to -/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms -/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require -/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the -/// feature. -public enum OpenLRSideOfRoad: Equatable, Sendable { - /// The relationship between the road object and the road is unknown. - case unknown - /// The road object is on the right side of the road. - case right - /// The road object is on the left side of the road. - case left - /// The road object is on both sides of the road or directly on the road. - case both - - init(_ native: MapboxNavigationNative.SideOfRoad) { - switch native { - case .onRoadOrUnknown: - self = .unknown - case .right: - self = .right - case .left: - self = .left - case .both: - self = .both - @unknown default: - assertionFailure("Unknown OpenLRSideOfRoad value.") - self = .unknown - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraph.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraph.swift deleted file mode 100644 index 3feb72d3e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraph.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Foundation -import MapboxNavigationNative -import Turf - -/// ``RoadGraph`` provides methods to get edge shape (e.g. ``RoadGraph/Edge``) and metadata. -/// -/// You do not create a ``RoadGraph`` object manually. Instead, use the ``RoadMatching/roadGraph`` from -/// ``ElectronicHorizonController/roadMatching`` -/// -/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to -/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms -/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require -/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the -/// feature. -public final class RoadGraph: Sendable { - // MARK: Getting Edge Info - - /// Returns metadata about the edge with the given edge identifier. - /// - Parameter edgeIdentifier: The identifier of the edge to query. - /// - Returns: Metadata about the edge with the given edge identifier, or `nil` if the edge is not in the cache. - public func edgeMetadata(edgeIdentifier: Edge.Identifier) -> Edge.Metadata? { - if let edgeMetadata = native.getEdgeMetadata(forEdgeId: UInt64(edgeIdentifier)) { - return Edge.Metadata(edgeMetadata) - } - return nil - } - - /// Returns a line string geometry corresponding to the given edge identifier. - /// - /// - Parameter edgeIdentifier: The identifier of the edge to query. - /// - Returns: A line string corresponding to the given edge identifier, or `nil` if the edge is not in the cache. - public func edgeShape(edgeIdentifier: Edge.Identifier) -> LineString? { - guard let locations = native.getEdgeShape(forEdgeId: UInt64(edgeIdentifier)) else { - return nil - } - return LineString(locations.map(\.value)) - } - - // MARK: Retrieving the Shape of an Object - - /// Returns a line string geometry corresponding to the given path. - /// - /// - Parameter path: The path of the geometry. - /// - Returns: A line string corresponding to the given path, or `nil` if any of path edges are not in the cache. - public func shape(of path: Path) -> LineString? { - guard let locations = native.getPathShape(for: GraphPath(path)) else { - return nil - } - return LineString(locations.map(\.value)) - } - - /// Returns a point corresponding to the given position. - /// - /// - Parameter position: The position of the point. - /// - Returns: A point corresponding to the given position, or `nil` if the edge is not in the cache. - public func shape(of position: Position) -> Point? { - guard let location = native.getPositionCoordinate(for: GraphPosition(position)) else { - return nil - } - return Point(location.value) - } - - init(_ native: GraphAccessor) { - self.native = native - } - - private let native: GraphAccessor -} - -extension GraphAccessor: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdge.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdge.swift deleted file mode 100644 index a2fe32038..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdge.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import MapboxNavigationNative - -extension RoadGraph { - /// An edge in a routing graph. For example, an edge may represent a road segment between two intersections or - /// between the two ends of a bridge. An edge may traverse multiple road objects, and a road object may be - /// associated with multiple edges. - /// - /// An electronic horizon is a probable path (or paths) of a vehicle. The road network ahead of the user is - /// represented as a tree of edges. Each intersection has outlet edges. In turn, each edge has a probability of - /// transition to another edge, as well as details about the road segment that the edge traverses. You can use these - /// details to influence application behavior based on predicted upcoming conditions. - /// - /// During active turn-by-turn navigation, the user-selected route and its metadata influence the path of the - /// electronic horizon. During passive navigation (free-driving), no route is actively selected, so the SDK will - /// determine the most probable path from the vehicle’s current location. You can receive notifications about - /// changes in the current state of the electronic horizon by observing the - /// ``Foundation/NSNotification/Name/electronicHorizonDidUpdatePosition``, - /// ``Foundation/NSNotification/Name/electronicHorizonDidEnterRoadObject``, and - /// ``Foundation/NSNotification/Name/electronicHorizonDidExitRoadObject`` notifications. - /// - /// Use a ``RoadGraph`` object to get an edge with a given identifier. - /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to - /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox - /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and - /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level - /// of use of the feature. - public struct Edge: Equatable, Sendable { - /// Unique identifier of a directed edge. - /// - /// Use a ``RoadGraph`` object to get more information about the edge with a given identifier. - public typealias Identifier = UInt - - /// Unique identifier of the directed edge. - public let identifier: Identifier - - /// The level of the edge. - /// - /// A value of 0 indicates that the edge is part of the most probable path (MPP), a value of 1 indicates an edge - /// that branches away from the MPP, and so on. - public let level: UInt - - /// The probability that the user will transition onto this edge, with 1 being certain and 0 being unlikely. - public let probability: Double - - /// The edges to which the user could transition from this edge. - /// - /// The most probable path may be split at some point if some of edges have a low probability difference - /// (±0.05). For example, ``RoadGraph/Edge/outletEdges`` can contain more than one edge with - /// ``RoadGraph/Edge/level`` set to 0. Currently, there is a maximum limit of one split per electronic horizon. - public let outletEdges: [Edge] - - /// Initializes a new ``RoadGraph/Edge`` object. - /// - Parameters: - /// - identifier: The unique identifier of a directed edge.: - /// - level: The level of the edge.: - /// - probability: The probability that the user will transition onto this edge.: - /// - outletEdges: The edges to which the user could transition from this edge. - public init(identifier: Identifier, level: UInt, probability: Double, outletEdges: [Edge]) { - self.identifier = identifier - self.level = level - self.probability = probability - self.outletEdges = outletEdges - } - - init(_ native: ElectronicHorizonEdge) { - self.identifier = UInt(native.id) - self.level = UInt(native.level) - self.probability = native.probability - self.outletEdges = native.out.map(Edge.init) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdgeMetadata.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdgeMetadata.swift deleted file mode 100644 index 2363f8a05..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphEdgeMetadata.swift +++ /dev/null @@ -1,195 +0,0 @@ -import CoreLocation -import Foundation -import MapboxDirections -import MapboxNavigationNative - -extension RoadGraph.Edge { - /// Indicates how many directions the user may travel along an edge. - /// - /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to - /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox - /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and - /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level - /// of use of the feature. - public enum Directionality: Sendable { - /// The user may only travel in one direction along the edge. - case oneWay - /// The user may travel in either direction along the edge. - case bothWays - } - - /// Edge metadata - public struct Metadata: Sendable { - // MARK: Geographical & Physical Characteristics - - /// The bearing in degrees clockwise at the start of the edge. - public let heading: CLLocationDegrees - - /// The edge’s length in meters. - public let length: CLLocationDistance - - /// The edge’s mean elevation, measured in meters. - public let altitude: CLLocationDistance? - - /// The edge’s curvature. - public let curvature: UInt - - // MARK: Road Classification - - /// Is the edge a bridge? - public let isBridge: Bool - - /// The edge’s general road classes. - public let roadClasses: RoadClasses - - /// The edge’s functional road class, according to the [Mapbox Streets - /// source](https://docs.mapbox.com/vector-tiles/reference/mapbox-streets-v8/#road), version 8. - public let mapboxStreetsRoadClass: MapboxStreetsRoadClass - - // MARK: Legal definitions - - /// The edge's names - public let names: [RoadName] - - /// The ISO 3166-1 alpha-2 code of the country where this edge is located. - public let countryCode: String? - - /// The ISO 3166-2 code of the country subdivision where this edge is located. - public let regionCode: String? - - // MARK: Road Regulations - - /// Indicates how many directions the user may travel along the edge. - public let directionality: Directionality - - /// The edge’s maximum speed limit. - public let speedLimit: Measurement? - - /// The user’s expected average speed along the edge, measured in meters per second. - public let speed: CLLocationSpeed - - /// Indicates which side of a bidirectional road on which the driver must be driving. Also referred to as the - /// rule of the road. - public let drivingSide: DrivingSide - - /// The number of parallel traffic lanes along the edge. - public let laneCount: UInt? - - /// `true` if edge is considered to be in an urban area, `false` otherwise. - public let isUrban: Bool - - /// Initializes a new edge ``RoadGraph/Edge/Metadata`` object. - public init( - heading: CLLocationDegrees, - length: CLLocationDistance, - roadClasses: RoadClasses, - mapboxStreetsRoadClass: MapboxStreetsRoadClass, - speedLimit: Measurement?, - speed: CLLocationSpeed, - isBridge: Bool, - names: [RoadName], - laneCount: UInt?, - altitude: CLLocationDistance?, - curvature: UInt, - countryCode: String?, - regionCode: String?, - drivingSide: DrivingSide, - directionality: Directionality, - isUrban: Bool - ) { - self.heading = heading - self.length = length - self.roadClasses = roadClasses - self.mapboxStreetsRoadClass = mapboxStreetsRoadClass - self.speedLimit = speedLimit - self.speed = speed - self.isBridge = isBridge - self.names = names - self.laneCount = laneCount - self.altitude = altitude - self.curvature = curvature - self.countryCode = countryCode - self.regionCode = regionCode - self.drivingSide = drivingSide - self.directionality = directionality - self.isUrban = isUrban - } - - /// Initializes a new edge ``RoadGraph/Edge/Metadata`` object. - init( - heading: CLLocationDegrees, - length: CLLocationDistance, - roadClasses: RoadClasses, - mapboxStreetsRoadClass: MapboxStreetsRoadClass, - speedLimit: Measurement?, - speed: CLLocationSpeed, - isBridge: Bool, - names: [RoadName], - laneCount: UInt?, - altitude: CLLocationDistance?, - curvature: UInt, - countryCode: String?, - regionCode: String?, - drivingSide: DrivingSide, - directionality: Directionality - ) { - self.init( - heading: heading, - length: length, - roadClasses: roadClasses, - mapboxStreetsRoadClass: mapboxStreetsRoadClass, - speedLimit: speedLimit, - speed: speed, - isBridge: isBridge, - names: names, - laneCount: laneCount, - altitude: altitude, - curvature: curvature, - countryCode: countryCode, - regionCode: regionCode, - drivingSide: drivingSide, - directionality: directionality, - isUrban: false - ) - } - - init(_ native: EdgeMetadata) { - self.heading = native.heading - self.length = native.length - self.mapboxStreetsRoadClass = MapboxStreetsRoadClass(native.frc, isRamp: native.ramp) - if let speedLimitValue = native.speedLimit as? Double { - // TODO: Convert to miles per hour as locally appropriate. - self.speedLimit = Measurement( - value: speedLimitValue == 0.0 ? .infinity : speedLimitValue, - unit: UnitSpeed.metersPerSecond - ).converted(to: .kilometersPerHour) - } else { - self.speedLimit = nil - } - self.speed = native.speed - - var roadClasses: RoadClasses = [] - if native.motorway { - roadClasses.update(with: .motorway) - } - if native.tunnel { - roadClasses.update(with: .tunnel) - } - if native.toll { - roadClasses.update(with: .toll) - } - self.roadClasses = roadClasses - - self.isBridge = native.bridge - self.names = native.names.compactMap(RoadName.init) - self.laneCount = native.laneCount as? UInt - self.altitude = native.meanElevation as? Double - self.curvature = UInt(native.curvature) - self.countryCode = native.countryCodeIso2 - self.regionCode = native.stateCode - self.drivingSide = native.isRightHandTraffic ? .right : .left - self.directionality = native.isOneway ? .oneWay : .bothWays - self.isUrban = native.isUrban - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPath.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPath.swift deleted file mode 100644 index 65b631f53..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPath.swift +++ /dev/null @@ -1,66 +0,0 @@ -import CoreLocation -import Foundation -import MapboxNavigationNative - -extension RoadGraph { - /// A position along a linear object in the road graph. - /// - /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to - /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox - /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and - /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level - /// of use of the feature. - public struct Path: Equatable, Sendable { - /// The edge identifiers that fully or partially coincide with the linear object. - public let edgeIdentifiers: [Edge.Identifier] - - /// The distance from the start of an edge to the start of the linear object as a fraction of the edge’s length - /// from 0 to 1. - public let fractionFromStart: Double - - /// The distance from the end of the linear object to the end of an edge as a fraction of the edge’s length from - /// 0 to 1. - public let fractionToEnd: Double - - /// Length of a path, measured in meters. - public let length: CLLocationDistance - - /// Initializes a new ``RoadGraph/Path`` object. - /// - Parameters: - /// - edgeIdentifiers: An `Array` of edge identifiers that fully or partially coincide with the linear object. - /// - fractionFromStart: The distance from the start of an edge to the start of the linear object as a - /// fraction of the edge's length from 0 to 1. - /// - fractionToEnd: The distance from the end of the linear object to the edge of the edge as a fraction of - /// the edge's length from 0 to 1. - /// - length: Length of a ``RoadGraph/Path`` measured in meters. - public init( - edgeIdentifiers: [RoadGraph.Edge.Identifier], - fractionFromStart: Double, - fractionToEnd: Double, - length: CLLocationDistance - ) { - self.edgeIdentifiers = edgeIdentifiers - self.fractionFromStart = fractionFromStart - self.fractionToEnd = fractionToEnd - self.length = length - } - - init(_ native: GraphPath) { - self.edgeIdentifiers = native.edges.map(\.uintValue) - self.fractionFromStart = native.percentAlongBegin - self.fractionToEnd = native.percentAlongEnd - self.length = native.length - } - } -} - -extension GraphPath { - convenience init(_ path: RoadGraph.Path) { - self.init( - edges: path.edgeIdentifiers.map { NSNumber(value: $0) }, - percentAlongBegin: path.fractionFromStart, - percentAlongEnd: path.fractionToEnd, - length: path.length - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPosition.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPosition.swift deleted file mode 100644 index 325e0e98a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadGraphPosition.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import MapboxNavigationNative - -extension RoadGraph { - /// The position of a point object in the road graph. - /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to - /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox - /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and - /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level - /// of use of the feature. - public struct Position: Equatable, Sendable { - /// The edge identifier along which the point object lies. - public let edgeIdentifier: Edge.Identifier - - /// The distance from the start of an edge to the point object as a fraction of the edge’s length from 0 to 1. - public let fractionFromStart: Double - - /// Initializes a new ``RoadGraph/Position`` object with a given edge identifier and fraction from the start of - /// the edge. - /// - Parameters: - /// - edgeIdentifier: The edge identifier. - /// - fractionFromStart: The fraction from the start of the edge. - public init(edgeIdentifier: RoadGraph.Edge.Identifier, fractionFromStart: Double) { - self.edgeIdentifier = edgeIdentifier - self.fractionFromStart = fractionFromStart - } - - init(_ native: GraphPosition) { - self.edgeIdentifier = UInt(native.edgeId) - self.fractionFromStart = native.percentAlong - } - } -} - -extension GraphPosition { - convenience init(_ position: RoadGraph.Position) { - self.init( - edgeId: UInt64(position.edgeIdentifier), - percentAlong: position.fractionFromStart - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadName.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadName.swift deleted file mode 100644 index 4a63c835c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadName.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// Road information, like Route number, street name, shield information, etc. -/// -/// - note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta -/// and is subject to changes, including its pricing. Use of the feature is subject to the beta product restrictions -/// in the Mapbox Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at -/// any time and require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of -/// the level of use of the feature. -public struct RoadName: Equatable, Sendable { - /// The name of the road. - /// - /// If you display a name to the user, you may need to abbreviate common words like “East” or “Boulevard” to ensure - /// that it fits in the allotted space. - public let text: String - - /// IETF BCP 47 language tag or "Unspecified" or empty string. - public let language: String - - /// Shield information of the road. - public let shield: RoadShield? - - /// Creates a new `RoadName` instance. - /// - Parameters: - /// - text: The name of the road. - /// - language: IETF BCP 47 language tag or "Unspecified" or empty string. - /// - shield: Shield information of the road. - public init(text: String, language: String, shield: RoadShield? = nil) { - self.text = text - self.language = language - self.shield = shield - } - - init?(_ native: MapboxNavigationNative.RoadName) { - guard native.text != "/" else { return nil } - - self.shield = native.shield.map(RoadShield.init) - self.text = native.text - self.language = native.language - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObject.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObject.swift deleted file mode 100644 index 5b8224cd3..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObject.swift +++ /dev/null @@ -1,78 +0,0 @@ -import CoreLocation -import Foundation -@preconcurrency import MapboxNavigationNative - -public struct RoadObjectAhead: Equatable, Sendable { - public var roadObject: RoadObject - public var distance: CLLocationDistance? - - public init(roadObject: RoadObject, distance: CLLocationDistance? = nil) { - self.roadObject = roadObject - self.distance = distance - } -} - -/// Describes the object on the road. -/// There are two sources of road objects: active route and the electronic horizon. -public struct RoadObject: Equatable, Sendable { - /// Identifier of the road object. If we get the same objects (e.g. ``RoadObject/Kind/tunnel(_:)``) from the - /// electronic horizon and the active route, they will not have the same IDs. - public let identifier: RoadObject.Identifier - - /// Length of the object, `nil` if the object is point-like. - public let length: CLLocationDistance? - - /// Location of the road object. - public let location: RoadObject.Location - - /// Kind of the road object with metadata. - public let kind: RoadObject.Kind - - /// `true` if an object is added by user, `false` if it comes from Mapbox service. - public let isUserDefined: Bool - - /// Indicates whether the road object is located in an urban area. - /// This property is set to `nil` if the road object comes from a call to the - /// ``RoadObjectStore/roadObject(identifier:)`` method and ``RoadObject/location`` is set to - /// ``RoadObject/Location/point(position:)``. - public let isUrban: Bool? - - let native: MapboxNavigationNative.RoadObject? - - /// Initializes a new `RoadObject` object. - public init( - identifier: RoadObject.Identifier, - length: CLLocationDistance?, - location: RoadObject.Location, - kind: RoadObject.Kind, - isUrban: Bool? - ) { - self.identifier = identifier - self.length = length - self.location = location - self.kind = kind - self.isUserDefined = true - self.isUrban = isUrban - self.native = nil - } - - /// Initializes a new ``RoadObject`` object. - init( - identifier: RoadObject.Identifier, - length: CLLocationDistance?, - location: RoadObject.Location, - kind: RoadObject.Kind - ) { - self.init(identifier: identifier, length: length, location: location, kind: kind, isUrban: nil) - } - - public init(_ native: MapboxNavigationNative.RoadObject) { - self.identifier = native.id - self.length = native.length?.doubleValue - self.location = RoadObject.Location(native.location) - self.kind = RoadObject.Kind(type: native.type, metadata: native.metadata) - self.isUserDefined = native.provider == .custom - self.isUrban = native.isUrban?.boolValue - self.native = native - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectEdgeLocation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectEdgeLocation.swift deleted file mode 100644 index 447b84fa1..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectEdgeLocation.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import MapboxNavigationNative - -extension RoadObject { - /// Represents location of road object on road graph. - /// - /// A point object is represented by a single edge whose location has the same ``fractionFromStart`` and - /// ``fractionToEnd``. - /// - /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to - /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox - /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and - /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level - /// of use of the feature. - public struct EdgeLocation { - /// Offset from the start of edge (0 - 1) pointing to the beginning of road object on this edge will be 0 for - /// all edges in the line-like road object except the very first one in the case of point-like object - /// fractionFromStart == fractionToEnd. - public let fractionFromStart: Double - - /// Offset from the start of edge (0 - 1) pointing to the end of road object on this edge will be 1 for all - /// edges in the line-like road object except the very first one in the case of point-like object - /// fractionFromStart == fractionToEnd. - public let fractionToEnd: Double - - /// Initializes a new ``RoadObject/EdgeLocation`` object with a fraction from the start and a fraction from the - /// end of the road object. - public init(fractionFromStart: Double, fractionToEnd: Double) { - self.fractionFromStart = fractionFromStart - self.fractionToEnd = fractionToEnd - } - - init(_ native: MapboxNavigationNative.RoadObjectEdgeLocation) { - self.fractionFromStart = native.percentAlongBegin - self.fractionToEnd = native.percentAlongEnd - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectKind.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectKind.swift deleted file mode 100644 index d6a06b7b2..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectKind.swift +++ /dev/null @@ -1,124 +0,0 @@ -import Foundation -import MapboxDirections -import MapboxNavigationNative - -extension RoadObject { - /// Type of the road object. - public enum Kind: Equatable, @unchecked Sendable { - /// An alert providing information about incidents on a route. Incidents can include *congestion*, - /// *massTransit*, and more (see `Incident.Kind` for the full list of incident types). - case incident(Incident?) - - /// An alert describing a point along the route where a toll may be collected. Note that this does not describe - /// the entire toll road, rather it describes a booth or electronic gate where a toll is typically charged. - case tollCollection(TollCollection?) - - /// An alert describing a country border crossing along the route. The alert triggers at the point where the - /// administrative boundary changes from one country to another. Two-letter and three-letter ISO 3166-1 country - /// codes are provided for the exiting country and the entering country. See ``BorderCrossing``. - case borderCrossing(BorderCrossing?) - - /// An alert describing a section of the route that continues through a tunnel. The alert begins at the entrance - /// of the tunnel and ends at the exit of the tunnel. For named tunnels, the tunnel name is provided as part of - /// ``Tunnel/name``. - case tunnel(Tunnel?) - - /// An alert about a rest area or service area accessible from the route. The alert marks the point along the - /// route where a driver can choose to pull off to access a rest stop. See `MapboxDirections.StopType`. - case serviceArea(RestStop?) - - /// An alert about a segment of a route that includes a restriction. Restricted roads can include private - /// access roads or gated areas that can be accessed but are not open to vehicles passing through. - case restrictedArea - - /// An alert about a segment of a route that includes a bridge. - case bridge - - /// An alert about a railroad crossing at grade, also known as a level crossing. - case railroadCrossing - - /// A road alert that was added by the user via ``RoadObjectStore/addUserDefinedRoadObject(_:)``, - case userDefined - - /// Japan-specific interchange info, refers to an expressway entrance and exit, e.g. Wangannarashino IC. - case ic(Interchange?) - - /// Japan-specific junction info, refers to a place where multiple expressways meet, e.g. Ariake JCT. - case jct(Junction?) - - /// Undefined. - case undefined - - init(_ native: MapboxNavigationNative.RoadObjectType) { - switch native { - case .incident: - self = .incident(nil) - case .tollCollectionPoint: - self = .tollCollection(nil) - case .borderCrossing: - self = .borderCrossing(nil) - case .tunnel: - self = .tunnel(nil) - case .serviceArea: - self = .serviceArea(nil) - case .restrictedArea: - self = .restrictedArea - case .bridge: - self = .bridge - case .railwayCrossing: - self = .railroadCrossing - case .custom: - self = .userDefined - case .ic: - self = .ic(nil) - case .jct: - self = .jct(nil) - case .notification: - self = .undefined - case .mergingArea: - self = .undefined - @unknown default: - self = .undefined - } - } - - init(type: MapboxNavigationNative.RoadObjectType, metadata: MapboxNavigationNative.RoadObjectMetadata) { - switch type { - case .incident: - self = .incident(metadata.isIncidentInfo() ? Incident(metadata.getIncidentInfo()) : nil) - case .tollCollectionPoint: - self = .tollCollection( - metadata - .isTollCollectionInfo() ? TollCollection(metadata.getTollCollectionInfo()) : nil - ) - case .borderCrossing: - self = .borderCrossing( - metadata - .isBorderCrossingInfo() ? BorderCrossing(metadata.getBorderCrossingInfo()) : nil - ) - case .tunnel: - self = .tunnel(metadata.isTunnelInfo() ? Tunnel(metadata.getTunnelInfo()) : nil) - case .serviceArea: - self = .serviceArea(metadata.isServiceAreaInfo() ? RestStop(metadata.getServiceAreaInfo()) : nil) - case .restrictedArea: - self = .restrictedArea - case .bridge: - self = .bridge - case .railwayCrossing: - self = .railroadCrossing - case .custom: - self = .userDefined - case .ic: - self = .ic(metadata.isIcInfo() ? Interchange(metadata.getIcInfo()) : nil) - case .jct: - self = .jct(metadata.isJctInfo() ? Junction(metadata.getJctInfo()) : nil) - case .notification: - self = .undefined - case .mergingArea: - self = .undefined - @unknown default: - self = .undefined - } - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectLocation.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectLocation.swift deleted file mode 100644 index b4c462b14..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectLocation.swift +++ /dev/null @@ -1,127 +0,0 @@ -import Foundation -import MapboxNavigationNative -import Turf - -extension RoadObject { - /// The location of a road object in the road graph. - public enum Location: Equatable, Sendable { - /// Location of an object represented as a gantry. - /// - Parameters: - /// - positions: Positions of gantry entries. - /// - shape: Shape of a gantry. - case gantry(positions: [RoadObject.Position], shape: Turf.Geometry) - - /// Location of an object represented as a point. - /// - position: Position of the object on the road graph. - case point(position: RoadObject.Position) - - /// Location of an object represented as a polygon. - /// - Parameters: - /// - entries: Positions of polygon entries. - /// - exits: Positions of polygon exits. - /// - shape: Shape of a polygon. - case polygon( - entries: [RoadObject.Position], - exits: [RoadObject.Position], - shape: Turf.Geometry - ) - - /// Location of an object represented as a polyline. - /// - Parameters: - /// - path: Position of a polyline on a road graph. - /// - shape: Shape of a polyline. - case polyline(path: RoadGraph.Path, shape: Turf.Geometry) - - /// Location of an object represented as a subgraph. - /// - Parameters: - /// - enters: Positions of the subgraph enters. - /// - exits: Positions of the subgraph exits. - /// - shape: Shape of a subgraph. - /// - edges: Edges of the subgraph associated by id. - case subgraph( - enters: [RoadObject.Position], - exits: [RoadObject.Position], - shape: Turf.Geometry, - edges: [RoadGraph.SubgraphEdge.Identifier: RoadGraph.SubgraphEdge] - ) - - /// Location of an object represented as an OpenLR line. - /// - Parameters: - /// - path: Position of a line on a road graph. - /// - shape: Shape of a line. - case openLRLine(path: RoadGraph.Path, shape: Turf.Geometry) - - /// Location of an object represented as an OpenLR point. - /// - Parameters: - /// - position: Position of the point on the graph. - /// - sideOfRoad: Specifies on which side of road the point is located. - /// - orientation: Specifies orientation of the object relative to referenced line. - /// - coordinate: Map coordinate of the point. - case openLRPoint( - position: RoadGraph.Position, - sideOfRoad: OpenLRSideOfRoad, - orientation: OpenLROrientation, - coordinate: CLLocationCoordinate2D - ) - - /// Location of a route alert. - /// - Parameter shape: Shape of an object. - case routeAlert(shape: Turf.Geometry) - - init(_ native: MapboxNavigationNative.MatchedRoadObjectLocation) { - switch native.type { - case .openLRLineLocation: - let location = native.getOpenLRLineLocation() - self = .openLRLine( - path: RoadGraph.Path(location.getPath()), - shape: Geometry(location.getShape()) - ) - case .openLRPointAlongLineLocation: - let location = native.getOpenLRPointAlongLineLocation() - self = .openLRPoint( - position: RoadGraph.Position(location.getPosition()), - sideOfRoad: OpenLRSideOfRoad(location.getSideOfRoad()), - orientation: OpenLROrientation(location.getOrientation()), - coordinate: location.getCoordinate() - ) - case .matchedPolylineLocation: - let location = native.getMatchedPolylineLocation() - self = .polyline( - path: RoadGraph.Path(location.getPath()), - shape: Geometry(location.getShape()) - ) - case .matchedGantryLocation: - let location = native.getMatchedGantryLocation() - self = .gantry( - positions: location.getPositions().map(RoadObject.Position.init), - shape: Geometry(location.getShape()) - ) - case .matchedPolygonLocation: - let location = native.getMatchedPolygonLocation() - self = .polygon( - entries: location.getEntries().map(RoadObject.Position.init), - exits: location.getExits().map(RoadObject.Position.init), - shape: Geometry(location.getShape()) - ) - case .matchedPointLocation: - let location = native.getMatchedPointLocation() - self = .point(position: RoadObject.Position(location.getPosition())) - case .matchedSubgraphLocation: - let location = native.getMatchedSubgraphLocation() - let edges = location.getEdges() - .map { id, edge in (UInt(truncating: id), RoadGraph.SubgraphEdge(edge)) } - self = .subgraph( - enters: location.getEnters().map(RoadObject.Position.init), - exits: location.getExits().map(RoadObject.Position.init), - shape: Geometry(location.getShape()), - edges: .init(uniqueKeysWithValues: edges) - ) - case .routeAlertLocation: - let routeAlertLocation = native.getRouteAlert() - self = .routeAlert(shape: Geometry(routeAlertLocation.getShape())) - @unknown default: - preconditionFailure("RoadObjectLocation can't be constructed. Unknown type.") - } - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcher.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcher.swift deleted file mode 100644 index 4bd93a3ef..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcher.swift +++ /dev/null @@ -1,215 +0,0 @@ -import Foundation -import MapboxCommon_Private -import MapboxNavigationNative -import MapboxNavigationNative_Private -import Turf - -/// Provides methods for road object matching. -/// -/// Matching results are delivered asynchronously via a delegate. -/// In case of error (if there are no tiles in the cache, decoding failed, etc.) the object won't be matched. -/// Use the ``RoadMatching/roadObjectMatcher`` from ``ElectronicHorizonController/roadMatching`` to access the -/// currently active road object matcher. -/// -/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to -/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms -/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require -/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the -/// feature. -public final class RoadObjectMatcher: @unchecked Sendable { - // MARK: Matching Objects - - /// Matches given OpenLR object to the graph. - /// - Parameters: - /// - location: OpenLR location of the road object, encoded in a base64 string. - /// - identifier: Unique identifier of the object. - public func matchOpenLR(location: String, identifier: OpenLRIdentifier) { - let standard = MapboxNavigationNative.Standard(identifier: identifier) - let reference: RoadObject.Identifier = switch identifier { - case .tomTom(let ref): - ref - case .tpeg(let ref): - ref - } - let openLR = MatchableOpenLr( - openlr: OpenLR(base64Encoded: location, standard: standard), - id: reference - ) - native.matchOpenLRs(for: [openLR], options: MatchingOptions( - useOnlyPreloadedTiles: false, - allowPartialMatching: false, - partialPolylineDistanceCalculationStrategy: .onlyMatched - )) - } - - /// Matches given polyline to the graph. - /// Polyline should define a valid path on the graph, i.e. it should be possible to drive this path according to - /// traffic rules. - /// - Parameters: - /// - polyline: Polyline representing the object. - /// - identifier: Unique identifier of the object. - public func match(polyline: LineString, identifier: RoadObject.Identifier) { - let polyline = MatchableGeometry(id: identifier, coordinates: polyline.coordinates.map(Coordinate2D.init)) - native.matchPolylines( - forPolylines: [polyline], - options: MatchingOptions( - useOnlyPreloadedTiles: false, - allowPartialMatching: false, - partialPolylineDistanceCalculationStrategy: .onlyMatched - ) - ) - } - - /// Matches a given polygon to the graph. - /// "Matching" here means we try to find all intersections of the polygon with the road graph and track distances to - /// those intersections as distance to the polygon. - /// - Parameters: - /// - polygon: Polygon representing the object. - /// - identifier: Unique identifier of the object. - public func match(polygon: Polygon, identifier: RoadObject.Identifier) { - let polygone = MatchableGeometry( - id: identifier, - coordinates: polygon.outerRing.coordinates.map(Coordinate2D.init) - ) - native.matchPolygons(forPolygons: [polygone], options: MatchingOptions( - useOnlyPreloadedTiles: false, - allowPartialMatching: false, - partialPolylineDistanceCalculationStrategy: .onlyMatched - )) - } - - /// Matches given gantry (i.e. polyline orthogonal to the road) to the graph. - /// "Matching" here means we try to find all intersections of the gantry with the road graph and track distances to - /// those intersections as distance to the gantry. - /// - Parameters: - /// - gantry: Gantry representing the object. - /// - identifier: Unique identifier of the object. - public func match(gantry: MultiPoint, identifier: RoadObject.Identifier) { - let gantry = MatchableGeometry(id: identifier, coordinates: gantry.coordinates.map(Coordinate2D.init)) - native.matchGantries( - forGantries: [gantry], - options: MatchingOptions( - useOnlyPreloadedTiles: false, - allowPartialMatching: false, - partialPolylineDistanceCalculationStrategy: .onlyMatched - ) - ) - } - - /// Matches given point to road graph. - /// - Parameters: - /// - point: Point representing the object. - /// - identifier: Unique identifier of the object. - /// - heading: Heading of the provided point, which is going to be matched. - public func match(point: CLLocationCoordinate2D, identifier: RoadObject.Identifier, heading: CLHeading? = nil) { - var trueHeading: NSNumber? - if let heading, heading.trueHeading >= 0.0 { - trueHeading = NSNumber(value: heading.trueHeading) - } - - let matchablePoint = MatchablePoint(id: identifier, coordinate: point, heading: trueHeading) - native.matchPoints( - for: [matchablePoint], - options: MatchingOptions( - useOnlyPreloadedTiles: false, - allowPartialMatching: false, - partialPolylineDistanceCalculationStrategy: .onlyMatched - ) - ) - } - - /// Cancel road object matching. - /// - Parameter identifier: Identifier for which matching should be canceled. - public func cancel(identifier: RoadObject.Identifier) { - native.cancel(forIds: [identifier]) - } - - // MARK: Observing Matching Results - - /// Road object matcher delegate. - public weak var delegate: RoadObjectMatcherDelegate? { - didSet { - if delegate != nil { - internalRoadObjectMatcherListener.delegate = delegate - } else { - internalRoadObjectMatcherListener.delegate = nil - } - updateListener() - } - } - - private func updateListener() { - if delegate != nil { - native.setListenerFor(internalRoadObjectMatcherListener) - } else { - native.setListenerFor(nil) - } - } - - var native: MapboxNavigationNative.RoadObjectMatcher { - didSet { - updateListener() - } - } - - /// Object, which subscribes to events being sent from the ``RoadObjectMatcherListener``, and passes them to the - /// ``RoadObjectMatcherDelegate``. - var internalRoadObjectMatcherListener: InternalRoadObjectMatcherListener! - - init(_ native: MapboxNavigationNative.RoadObjectMatcher) { - self.native = native - - self.internalRoadObjectMatcherListener = InternalRoadObjectMatcherListener(roadObjectMatcher: self) - } - - deinit { - internalRoadObjectMatcherListener.delegate = nil - native.setListenerFor(nil) - } -} - -extension MapboxNavigationNative.RoadObjectMatcherError: Error, @unchecked Sendable {} - -extension CLLocation { - convenience init(coordinate: CLLocationCoordinate2D) { - self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) - } -} - -// Since `MBXExpected` cannot be exposed publicly `InternalRoadObjectMatcherListener` works as an -// intermediary by subscribing to the events from the `RoadObjectMatcherListener`, and passing them -// to the `RoadObjectMatcherDelegate`. -class InternalRoadObjectMatcherListener: RoadObjectMatcherListener { - weak var roadObjectMatcher: RoadObjectMatcher? - - weak var delegate: RoadObjectMatcherDelegate? - - init(roadObjectMatcher: RoadObjectMatcher) { - self.roadObjectMatcher = roadObjectMatcher - } - - public func onRoadObjectMatched( - forRoadObject roadObject: Expected< - MapboxNavigationNative.RoadObject, - MapboxNavigationNative.RoadObjectMatcherError - > - ) { - guard let roadObjectMatcher else { return } - - let result = Result< - MapboxNavigationNative.RoadObject, - MapboxNavigationNative.RoadObjectMatcherError - >(expected: roadObject) - switch result { - case .success(let roadObject): - delegate?.roadObjectMatcher(roadObjectMatcher, didMatch: RoadObject(roadObject)) - case .failure(let error): - delegate?.roadObjectMatcher(roadObjectMatcher, didFailToMatchWith: RoadObjectMatcherError(error)) - } - } - - func onMatchingCancelled(forId id: String) { - guard let roadObjectMatcher else { return } - delegate?.roadObjectMatcher(roadObjectMatcher, didCancelMatchingFor: id) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherDelegate.swift deleted file mode 100644 index 787b2373a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherDelegate.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// ``RoadObjectMatcher`` delegate. -/// -/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to -/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms -/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require -/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the -/// feature. -public protocol RoadObjectMatcherDelegate: AnyObject { - /// This method is called with a road object when the matching is successfully finished. - /// - Parameters: - /// - matcher: The ``RoadObjectMatcher`` instance. - /// - roadObject: The matched ``RoadObject`` instance. - func roadObjectMatcher(_ matcher: RoadObjectMatcher, didMatch roadObject: RoadObject) - - /// This method is called when the matching is finished with error. - /// - Parameters: - /// - matcher: The ``RoadObjectMatcher`` instance. - /// - error: The ``RoadObjectMatcherError`` occured. - func roadObjectMatcher(_ matcher: RoadObjectMatcher, didFailToMatchWith error: RoadObjectMatcherError) - - /// This method is called when the matching is canceled. - /// - Parameters: - /// - matcher: The ``RoadObjectMatcher`` instance. - /// - id: The id of the ``RoadObject``. - func roadObjectMatcher(_ matcher: RoadObjectMatcher, didCancelMatchingFor id: String) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherError.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherError.swift deleted file mode 100644 index 453b37fa4..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectMatcherError.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// An error that occures during road object matching. -/// -/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to -/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms -/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require -/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the -/// feature. -public struct RoadObjectMatcherError: LocalizedError { - /// Description of the error. - public let description: String - - /// Identifier of the road object for which matching is failed. - public let roadObjectIdentifier: RoadObject.Identifier - - /// Description of the error. - public var errorDescription: String? { - return description - } - - /// Initializes a new ``RoadObjectMatcherError``. - /// - Parameters: - /// - description: Description of the error. - /// - roadObjectIdentifier: Identifier of the road object for which matching is failed. - public init(description: String, roadObjectIdentifier: RoadObject.Identifier) { - self.description = description - self.roadObjectIdentifier = roadObjectIdentifier - } - - init(_ native: MapboxNavigationNative.RoadObjectMatcherError) { - self.description = native.description - self.roadObjectIdentifier = native.roadObjectId - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectPosition.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectPosition.swift deleted file mode 100644 index e26b77348..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectPosition.swift +++ /dev/null @@ -1,29 +0,0 @@ -import CoreLocation -import Foundation -import MapboxNavigationNative - -extension RoadObject { - /// Contains information about position of the point on the graph and it's geo-position. - public struct Position: Equatable, Sendable { - /// Position on the graph. - public let position: RoadGraph.Position - - /// Geo-position of the object. - public let coordinate: CLLocationCoordinate2D - - /// nitializes a new ``RoadObject/Position`` object with a given position on the graph and coordinate of the - /// object. - /// - Parameters: - /// - position: The position on the graph. - /// - coordinate: The location of the object. - public init(position: RoadGraph.Position, coordinate: CLLocationCoordinate2D) { - self.position = position - self.coordinate = coordinate - } - - init(_ native: MapboxNavigationNative.Position) { - self.position = RoadGraph.Position(native.position) - self.coordinate = native.coordinate - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStore.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStore.swift deleted file mode 100644 index 6f6a05ed6..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStore.swift +++ /dev/null @@ -1,126 +0,0 @@ -import Foundation -import MapboxNavigationNative - -extension RoadObject { - /// Identifies a road object in an electronic horizon. A road object represents a notable transition point along a - /// road, such as a toll booth or tunnel entrance. A road object is similar to a ``RouteAlert`` but is more closely - /// associated with the routing graph managed by the ``RoadGraph`` class. - /// - /// Use a ``RoadObjectStore`` object to get more information about a road object with a given identifier or get the - /// locations of road objects along ``RoadGraph/Edge``s. - public typealias Identifier = String -} - -/// Stores and provides access to metadata about road objects. -/// -/// You do not create a ``RoadObjectStore`` object manually. Instead, use the ``RoadMatching/roadObjectStore`` from -/// ``ElectronicHorizonController/roadMatching`` to access the currently active road object store. -/// -/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to -/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms -/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require -/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the -/// feature. -public final class RoadObjectStore: @unchecked Sendable { - /// The road object store’s delegate. - public weak var delegate: RoadObjectStoreDelegate? { - didSet { - updateObserver() - } - } - - // MARK: Getting Road Objects Data - - /// - Parameter edgeIdentifier: The identifier of the edge to query. - /// - Returns: Returns mapping `road object identifier ->` ``RoadObject/EdgeLocation`` for all road objects which - /// are lying on the edge with given identifier. - public func roadObjectEdgeLocations( - edgeIdentifier: RoadGraph.Edge - .Identifier - ) -> [RoadObject.Identifier: RoadObject.EdgeLocation] { - let objects = native.getForEdgeId(UInt64(edgeIdentifier)) - return objects.mapValues(RoadObject.EdgeLocation.init) - } - - /// Since road objects can be removed/added in background we should always check return value for `nil`, even if we - /// know that we have object with such identifier based on previous calls. - /// - Parameter roadObjectIdentifier: The identifier of the road object to query. - /// - Returns: Road object with given identifier, if such object cannot be found returns `nil`. - public func roadObject(identifier roadObjectIdentifier: RoadObject.Identifier) -> RoadObject? { - if let roadObject = native.getRoadObject(forId: roadObjectIdentifier) { - return RoadObject(roadObject) - } - return nil - } - - /// Returns the list of road object ids which are (partially) belong to `edgeIds`. - /// - Parameter edgeIdentifiers: The list of edge ids. - /// - Returns: The list of road object ids which are (partially) belong to `edgeIds`. - public func roadObjectIdentifiers(edgeIdentifiers: [RoadGraph.Edge.Identifier]) -> [RoadObject.Identifier] { - return native.getRoadObjectIdsByEdgeIds(forEdgeIds: edgeIdentifiers.map(NSNumber.init)) - } - - // MARK: Managing Custom Road Objects - - /// Adds a road object to be tracked in the electronic horizon. In case if an object with such identifier already - /// exists, updates it. - /// - Note: a road object obtained from route alerts cannot be added via this API. - /// - Parameter roadObject: Custom road object, acquired from ``RoadObjectMatcher``. - public func addUserDefinedRoadObject(_ roadObject: RoadObject) { - guard let nativeObject = roadObject.native else { - preconditionFailure("You can only add matched a custom road object, acquired from RoadObjectMatcher.") - } - native.addCustomRoadObject(for: nativeObject) - } - - /// Removes road object and stops tracking it in the electronic horizon. - /// - Parameter identifier: Identifier of the road object that should be removed. - public func removeUserDefinedRoadObject(identifier: RoadObject.Identifier) { - native.removeCustomRoadObject(forId: identifier) - } - - /// Removes all user-defined road objects from the store and stops tracking them in the electronic horizon. - public func removeAllUserDefinedRoadObjects() { - native.removeAllCustomRoadObjects() - } - - init(_ native: MapboxNavigationNative.RoadObjectsStore) { - self.native = native - } - - deinit { - if native.hasObservers() { - native.removeObserver(for: self) - } - } - - var native: MapboxNavigationNative.RoadObjectsStore { - didSet { - updateObserver() - } - } - - private func updateObserver() { - if delegate != nil { - native.addObserver(for: self) - } else { - if native.hasObservers() { - native.removeObserver(for: self) - } - } - } -} - -extension RoadObjectStore: RoadObjectsStoreObserver { - public func onRoadObjectAdded(forId id: String) { - delegate?.didAddRoadObject(identifier: id) - } - - public func onRoadObjectUpdated(forId id: String) { - delegate?.didUpdateRoadObject(identifier: id) - } - - public func onRoadObjectRemoved(forId id: String) { - delegate?.didRemoveRoadObject(identifier: id) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStoreDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStoreDelegate.swift deleted file mode 100644 index a8f0efaf6..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadObjectStoreDelegate.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -/// ``RoadObjectStore`` delegate. -/// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to -/// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox Terms -/// of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and require -/// customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level of use of the -/// feature. -public protocol RoadObjectStoreDelegate: AnyObject { - /// This method is called when a road object with the given identifier has been added to the road objects store. - /// - Parameter identifier: The identifier of the road object. - func didAddRoadObject(identifier: RoadObject.Identifier) - - /// This method is called when a road object with the given identifier has been updated in the road objects store. - /// - Parameter identifier: The identifier of the road object. - func didUpdateRoadObject(identifier: RoadObject.Identifier) - - /// This method is called when a road object with the given identifier has been removed from the road objects store. - /// - Parameter identifier: The identifier of the road object. - func didRemoveRoadObject(identifier: RoadObject.Identifier) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadShield.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadShield.swift deleted file mode 100644 index b09072d87..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadShield.swift +++ /dev/null @@ -1,42 +0,0 @@ -import MapboxNavigationNative - -/// Describes a road shield information. -/// -/// - note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta -/// and is subject to changes, including its pricing. Use of the feature is subject to the beta product restrictions -/// in the Mapbox Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at -/// any time and require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of -/// the level of use of the feature. -public struct RoadShield: Equatable, Sendable { - /// The base url for a shield image. - public let baseUrl: String - - /// The shield display reference. - public let displayRef: String - - /// The shield text. - public let name: String - - /// The string indicating the color of the text to be rendered on the route shield, e.g. "black". - public let textColor: String - - /// Creates a new `Shield` instance. - /// - Parameters: - /// - baseUrl: The base url for a shield image. - /// - displayRef: The shield display reference. - /// - name: The shield text. - /// - textColor: The string indicating the color of the text to be rendered on the route shield. - public init(baseUrl: String, displayRef: String, name: String, textColor: String) { - self.baseUrl = baseUrl - self.displayRef = displayRef - self.name = name - self.textColor = textColor - } - - init(_ native: MapboxNavigationNative.Shield) { - self.baseUrl = native.baseUrl - self.name = native.name - self.displayRef = native.displayRef - self.textColor = native.textColor - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadSubgraphEdge.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadSubgraphEdge.swift deleted file mode 100644 index 9ee68aa8f..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RoadSubgraphEdge.swift +++ /dev/null @@ -1,65 +0,0 @@ -import Foundation -import MapboxNavigationNative -import Turf - -extension RoadGraph { - /// The ``RoadGraph/SubgraphEdge`` represents an edge in the complex object which might be considered as a directed - /// graph. The graph might contain loops. ``innerEdgeIds`` and ``outerEdgeIds`` properties contain edge ids, which - /// allows to traverse the graph, obtain geometry and calculate different distances inside it. - /// - /// - Note: The Mapbox Electronic Horizon feature of the Mapbox Navigation SDK is in public beta and is subject to - /// changes, including its pricing. Use of the feature is subject to the beta product restrictions in the Mapbox - /// Terms of Service. Mapbox reserves the right to eliminate any free tier or free evaluation offers at any time and - /// require customers to place an order to purchase the Mapbox Electronic Horizon feature, regardless of the level - /// of use of the feature. - public struct SubgraphEdge: Equatable, Sendable { - /// Unique identifier of an edge. - /// - /// Use a ``RoadGraph`` object to get more information about the edge with a given identifier. - public typealias Identifier = Edge.Identifier - - /// Unique identifier of the edge. - public let identifier: Identifier - - /// The identifiers of edges in the subgraph from which the user could transition to this edge. - public let innerEdgeIds: [Identifier] - - /// The identifiers of edges in the subgraph to which the user could transition from this edge. - public let outerEdgeIds: [Identifier] - - /// The length of the edge mesured in meters. - public let length: CLLocationDistance - - /// The edge shape geometry. - public let shape: Turf.Geometry - - /// Initializes a new ``RoadGraph/SubgraphEdge`` object. - /// - Parameters: - /// - identifier: The unique identifier of an edge. - /// - innerEdgeIds: The edges from which the user could transition to this edge. - /// - outerEdgeIds: The edges to which the user could transition from this edge. - /// - length: The length of the edge mesured in meters. - /// - shape: The edge shape geometry. - public init( - identifier: Identifier, - innerEdgeIds: [Identifier], - outerEdgeIds: [Identifier], - length: CLLocationDistance, - shape: Turf.Geometry - ) { - self.identifier = identifier - self.innerEdgeIds = innerEdgeIds - self.outerEdgeIds = outerEdgeIds - self.length = length - self.shape = shape - } - - init(_ native: MapboxNavigationNative.SubgraphEdge) { - self.identifier = UInt(native.id) - self.innerEdgeIds = native.innerEdgeIds.map(UInt.init(truncating:)) - self.outerEdgeIds = native.outerEdgeIds.map(UInt.init(truncating:)) - self.length = native.length - self.shape = Turf.Geometry(native.shape) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RouteAlert.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RouteAlert.swift deleted file mode 100644 index 705f23620..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EHorizon/RouteAlert.swift +++ /dev/null @@ -1,22 +0,0 @@ -import CoreLocation -import Foundation -import MapboxDirections -import MapboxNavigationNative - -/// ``RouteAlert`` encapsulates information about various incoming events. Common attributes like location, distance to -/// the event, length and other is provided for each POI, while specific meta data is supplied via ``roadObject`` -/// property. -public struct RouteAlert: Equatable, Sendable { - /// Road object which describes upcoming route alert. - public let roadObject: RoadObject - - /// Distance from current position to alert, meters. - /// - /// This value can be negative if it is a spanned alert and we are somewhere in the middle of it. - public let distanceToStart: CLLocationDistance - - init(_ native: UpcomingRouteAlert, distanceToStart: CLLocationDistance) { - self.roadObject = RoadObject(native.roadObject) - self.distanceToStart = distanceToStart - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EtaDistanceInfo.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EtaDistanceInfo.swift deleted file mode 100644 index 9abe2724d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/EtaDistanceInfo.swift +++ /dev/null @@ -1,12 +0,0 @@ -import CoreLocation -import Foundation - -public struct EtaDistanceInfo: Equatable, Sendable { - public var distance: CLLocationDistance - public var travelTime: TimeInterval? - - public init(distance: CLLocationDistance, travelTime: TimeInterval?) { - self.distance = distance - self.travelTime = travelTime - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/FasterRouteController.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/FasterRouteController.swift deleted file mode 100644 index 8a68b3fe2..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/FasterRouteController.swift +++ /dev/null @@ -1,152 +0,0 @@ -import Combine -import CoreLocation -import Foundation -import MapboxDirections - -public protocol FasterRouteProvider: AnyObject { - var isRerouting: Bool { get set } - var navigationRoute: NavigationRoute? { get set } - var currentLocation: CLLocation? { get set } - var fasterRoutes: AnyPublisher { get } - - func checkForFasterRoute( - from routeProgress: RouteProgress - ) -} - -final class FasterRouteController: FasterRouteProvider, @unchecked Sendable { - struct Configuration { - let settings: FasterRouteDetectionConfig - let initialManeuverAvoidanceRadius: TimeInterval - let routingProvider: RoutingProvider - } - - let configuration: Configuration - - private var lastProactiveRerouteDate: Date? - private var routeTask: RoutingProvider.FetchTask? - - var isRerouting: Bool - var navigationRoute: NavigationRoute? - var currentLocation: CLLocation? - - private var _fasterRoutes: PassthroughSubject - var fasterRoutes: AnyPublisher { - _fasterRoutes.eraseToAnyPublisher() - } - - init(configuration: Configuration) { - self.configuration = configuration - - self.lastProactiveRerouteDate = nil - self.isRerouting = false - self.routeTask = nil - self._fasterRoutes = .init() - } - - func checkForFasterRoute( - from routeProgress: RouteProgress - ) { - Task { - guard let routeOptions = navigationRoute?.routeOptions, - let location = currentLocation else { return } - - // Only check for faster alternatives if the user has plenty of time left on the route. - guard routeProgress.durationRemaining > configuration.settings.minimumRouteDurationRemaining else { return } - // If the user is approaching a maneuver, don't check for a faster alternatives - guard routeProgress.currentLegProgress.currentStepProgress.durationRemaining > configuration.settings - .minimumManeuverOffset else { return } - - guard let currentUpcomingManeuver = routeProgress.currentLegProgress.upcomingStep else { - return - } - - guard let lastRouteValidationDate = lastProactiveRerouteDate else { - self.lastProactiveRerouteDate = location.timestamp - return - } - - // Only check every so often for a faster route. - guard location.timestamp.timeIntervalSince(lastRouteValidationDate) >= configuration.settings - .proactiveReroutingInterval - else { - return - } - - let durationRemaining = routeProgress.durationRemaining - - // Avoid interrupting an ongoing reroute - if isRerouting { return } - isRerouting = true - - defer { self.isRerouting = false } - - guard let navigationRoutes = await calculateRoutes( - from: location, - along: routeProgress, - options: routeOptions - ) else { - return - } - let route = navigationRoutes.mainRoute.route - - self.lastProactiveRerouteDate = nil - - guard let firstLeg = route.legs.first, let firstStep = firstLeg.steps.first else { - return - } - - let routeIsFaster = firstStep.expectedTravelTime >= self.configuration.settings.minimumManeuverOffset && - currentUpcomingManeuver == firstLeg.steps[1] && route.expectedTravelTime <= 0.9 * durationRemaining - - guard routeIsFaster else { - return - } - - let completion = { @MainActor in - self._fasterRoutes.send(navigationRoutes) - } - - switch self.configuration.settings.fasterRouteApproval { - case .automatically: - await completion() - case .manually(let approval): - if await approval((location, navigationRoutes.mainRoute)) { - await completion() - } - } - } - } - - private func calculateRoutes( - from origin: CLLocation, - along progress: RouteProgress, - options: RouteOptions - ) async -> NavigationRoutes? { - routeTask?.cancel() - - let options = progress.reroutingOptions(from: origin, routeOptions: options) - - // https://github.com/mapbox/mapbox-navigation-ios/issues/3966 - if isRerouting, - options.profileIdentifier == .automobile || options.profileIdentifier == .automobileAvoidingTraffic - { - options.initialManeuverAvoidanceRadius = configuration.initialManeuverAvoidanceRadius * origin.speed - } - - let task = configuration.routingProvider.calculateRoutes(options: options) - routeTask = task - defer { self.routeTask = nil } - - do { - let routes = try await task.value - return await routes.selectingMostSimilar(to: progress.route) - } catch { - Log.warning( - "Failed to fetch proactive reroute with error: \(error)", - category: .navigation - ) - return nil - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/CoreNavigator.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/CoreNavigator.swift deleted file mode 100644 index cc474d751..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/CoreNavigator.swift +++ /dev/null @@ -1,518 +0,0 @@ -import CoreLocation -import MapboxCommon_Private -import MapboxDirections -import MapboxNavigationNative -import UIKit - -protocol CoreNavigator { - var rawLocation: CLLocation? { get } - var mostRecentNavigationStatus: NavigationStatus? { get } - var tileStore: TileStore { get } - var roadGraph: RoadGraph { get } - var roadObjectStore: RoadObjectStore { get } - var roadObjectMatcher: RoadObjectMatcher { get } - var rerouteController: RerouteController? { get } - - @MainActor - func startUpdatingElectronicHorizon(with options: ElectronicHorizonConfig?) - @MainActor - func stopUpdatingElectronicHorizon() - - @MainActor - func setRoutes( - _ routesData: RoutesData, - uuid: UUID, - legIndex: UInt32, - reason: SetRoutesReason, - completion: @escaping @Sendable (Result) -> Void - ) - @MainActor - func setAlternativeRoutes( - with routes: [RouteInterface], - completion: @escaping @Sendable (Result<[RouteAlternative], Error>) -> Void - ) - @MainActor - func updateRouteLeg(to index: UInt32, completion: @escaping @Sendable (Bool) -> Void) - @MainActor - func unsetRoutes( - uuid: UUID, - completion: @escaping @Sendable (Result) -> Void - ) - - @MainActor - func updateLocation(_ location: CLLocation, completion: @escaping @Sendable (Bool) -> Void) - - @MainActor - func resume() - @MainActor - func pause() -} - -final class NativeNavigator: CoreNavigator, @unchecked Sendable { - struct Configuration: @unchecked Sendable { - let credentials: ApiConfiguration - let nativeHandlersFactory: NativeHandlersFactory - let routingConfig: RoutingConfig - let predictiveCacheManager: PredictiveCacheManager? - } - - let configuration: Configuration - - private(set) var navigator: NavigationNativeNavigator - private(set) var telemetrySessionManager: NavigationSessionManager - - private(set) var cacheHandle: CacheHandle - - var mostRecentNavigationStatus: NavigationStatus? { - navigatorStatusObserver?.mostRecentNavigationStatus - } - - private(set) var rawLocation: CLLocation? - - private(set) var tileStore: TileStore - - // Current Navigator status in terms of tile versioning. - var tileVersionState: NavigatorFallbackVersionsObserver.TileVersionState { - navigatorFallbackVersionsObserver?.tileVersionState ?? .nominal - } - - @MainActor - private lazy var routeCoordinator: RoutesCoordinator = .init( - routesSetupHandler: { @MainActor [weak self] routesData, legIndex, reason, completion in - - let dataParams = routesData.map { SetRoutesDataParams( - routes: $0, - legIndex: legIndex - ) } - - self?.navigator.native.setRoutesDataFor( - dataParams, - reason: reason - ) { [weak self] result in - if result.isValue(), - let routesResult = result.value - { - Log.info( - "Navigator has been updated, including \(routesResult.alternatives.count) alternatives.", - category: .navigation - ) - completion(.success((routesData?.primaryRoute().getRouteInfo(), routesResult.alternatives))) - } else if result.isError() { - let reason = (result.error as String?) ?? "" - Log.error("Failed to update navigator with reason: \(reason)", category: .navigation) - completion(.failure(NativeNavigatorError.failedToUpdateRoutes(reason: reason))) - } else { - assertionFailure("Invalid Expected value: \(result)") - completion(.failure( - NativeNavigatorError - .failedToUpdateRoutes(reason: "Unexpected internal response") - )) - } - } - }, - alternativeRoutesSetupHandler: { @MainActor [weak self] routes, completion in - self?.navigator.native.setAlternativeRoutesForRoutes(routes, callback: { result in - if result.isValue(), - let alternatives = result.value as? [RouteAlternative] - { - Log.info( - "Navigator alternative routes has been updated (\(alternatives.count) alternatives set).", - category: .navigation - ) - completion(.success(alternatives)) - } else { - let reason = (result.error as String?) ?? "" - Log.error( - "Failed to update navigator alternative routes with reason: \(reason)", - category: .navigation - ) - completion(.failure(NativeNavigatorError.failedToUpdateAlternativeRoutes(reason: reason))) - } - }) - } - ) - - private(set) var rerouteController: RerouteController? - - @MainActor - init(with configuration: Configuration) { - self.configuration = configuration - - let factory = configuration.nativeHandlersFactory - self.tileStore = factory.tileStore - self.cacheHandle = factory.cacheHandle - self.roadGraph = factory.roadGraph - self.navigator = factory.navigator - self.telemetrySessionManager = NavigationSessionManagerImp(navigator: navigator) - self.roadObjectStore = RoadObjectStore(navigator.native.roadObjectStore()) - self.roadObjectMatcher = RoadObjectMatcher(MapboxNavigationNative.RoadObjectMatcher(cache: cacheHandle)) - self.rerouteController = RerouteController( - configuration: .init( - credentials: configuration.credentials, - navigator: navigator, - configHandle: factory.configHandle(), - rerouteConfig: configuration.routingConfig.rerouteConfig, - initialManeuverAvoidanceRadius: configuration.routingConfig.initialManeuverAvoidanceRadius - ) - ) - - subscribeNavigator() - setupAlternativesControllerIfNeeded() - setupPredictiveCacheIfNeeded() - subscribeToNotifications() - } - - /// Destroys and creates new instance of Navigator together with other related entities. - /// - /// Typically, this method is used to restart a Navigator with a specific Version during switching to offline or - /// online modes. - /// - Parameter version: String representing a tile version name. `nil` value means "latest". Specifying exact - /// version also enables `fallback` mode which will passively monitor newer version available and will notify - /// `tileVersionState` if found. - @MainActor - func restartNavigator(forcing version: String? = nil) { - let previousNavigationSessionState = navigator.native.storeNavigationSession() - let previousSession = telemetrySessionManager as? NavigationSessionManagerImp - unsubscribeNavigator() - navigator.native.shutdown() - - let factory = configuration.nativeHandlersFactory.targeting(version: version) - - tileStore = factory.tileStore - cacheHandle = factory.cacheHandle - roadGraph = factory.roadGraph - navigator = factory.navigator - - navigator.native.restoreNavigationSession(for: previousNavigationSessionState) - telemetrySessionManager = NavigationSessionManagerImp(navigator: navigator, previousSession: previousSession) - roadObjectStore.native = navigator.native.roadObjectStore() - roadObjectMatcher.native = MapboxNavigationNative.RoadObjectMatcher(cache: cacheHandle) - rerouteController = RerouteController( - configuration: .init( - credentials: configuration.credentials, - navigator: navigator, - configHandle: factory.configHandle(), - rerouteConfig: configuration.routingConfig.rerouteConfig, - initialManeuverAvoidanceRadius: configuration.routingConfig.initialManeuverAvoidanceRadius - ) - ) - - subscribeNavigator() - setupAlternativesControllerIfNeeded() - setupPredictiveCacheIfNeeded() - } - - // MARK: - Subscriptions - - private weak var navigatorStatusObserver: NavigatorStatusObserver? - private weak var navigatorFallbackVersionsObserver: NavigatorFallbackVersionsObserver? - private weak var navigatorElectronicHorizonObserver: NavigatorElectronicHorizonObserver? - private weak var navigatorAlternativesObserver: NavigatorRouteAlternativesObserver? - private weak var navigatorRouteRefreshObserver: NavigatorRouteRefreshObserver? - - private func setupPredictiveCacheIfNeeded() { - guard let predictiveCacheManager = configuration.predictiveCacheManager, - case .nominal = tileVersionState else { return } - - Task { @MainActor in - predictiveCacheManager.updateNavigationController(with: navigator) - predictiveCacheManager.updateSearchController(with: navigator) - } - } - - @MainActor - private func setupAlternativesControllerIfNeeded() { - guard let alternativeRoutesDetectionConfig = configuration.routingConfig.alternativeRoutesDetectionConfig - else { return } - - guard let refreshIntervalSeconds = UInt16(exactly: alternativeRoutesDetectionConfig.refreshIntervalSeconds) - else { - assertionFailure("'refreshIntervalSeconds' has an unexpected value.") - return - } - - let configManeuverAvoidanceRadius = configuration.routingConfig.initialManeuverAvoidanceRadius - guard let initialManeuverAvoidanceRadius = Float(exactly: configManeuverAvoidanceRadius) else { - assertionFailure("'initialManeuverAvoidanceRadius' has an unexpected value.") - return - } - - navigator.native.getRouteAlternativesController().setRouteAlternativesOptionsFor( - RouteAlternativesOptions( - requestIntervalSeconds: refreshIntervalSeconds, - minTimeBeforeManeuverSeconds: initialManeuverAvoidanceRadius - ) - ) - } - - @MainActor - fileprivate func subscribeContinuousAlternatives() { - if configuration.routingConfig.alternativeRoutesDetectionConfig != nil { - let alternativesObserver = NavigatorRouteAlternativesObserver() - navigatorAlternativesObserver = alternativesObserver - navigator.native.getRouteAlternativesController().addObserver(for: alternativesObserver) - } else if let navigatorAlternativesObserver { - navigator.native.getRouteAlternativesController().removeObserver(for: navigatorAlternativesObserver) - self.navigatorAlternativesObserver = nil - } - } - - @MainActor - fileprivate func subscribeFallbackObserver() { - let versionsObserver = NavigatorFallbackVersionsObserver(restartCallback: { [weak self] targetVersion in - if let self { - _Concurrency.Task { @MainActor in - self.restartNavigator(forcing: targetVersion) - } - } - }) - navigatorFallbackVersionsObserver = versionsObserver - navigator.native.setFallbackVersionsObserverFor(versionsObserver) - } - - @MainActor - fileprivate func subscribeStatusObserver() { - let statusObserver = NavigatorStatusObserver() - navigatorStatusObserver = statusObserver - navigator.native.addObserver(for: statusObserver) - } - - @MainActor - fileprivate func subscribeElectornicHorizon() { - guard isSubscribedToElectronicHorizon else { - return - } - startUpdatingElectronicHorizon( - with: electronicHorizonConfig, - on: navigator - ) - } - - @MainActor - fileprivate func subscribeRouteRefreshing() { - guard let refreshPeriod = configuration.routingConfig.routeRefreshPeriod else { - return - } - - let refreshObserver = - NavigatorRouteRefreshObserver(refreshCallback: { [weak self] refreshResponse, routeId, geometryIndex in - return await withCheckedContinuation { continuation in - self?.navigator.native.refreshRoute( - forRouteRefreshResponse: refreshResponse, - routeId: routeId, - geometryIndex: geometryIndex - ) { result in - _Concurrency.Task { - if result.isValue() { - continuation.resume( - returning: RouteRefreshResult( - updatedRoute: result.value.route, - alternativeRoutes: result.value.alternatives - ) - ) - } else if result.isError(), - let error = result.error - { - Log.warning( - "Failed to apply route refresh response with error: \(error)", - category: .navigation - ) - continuation.resume(returning: nil) - } - } - } - } - }) - navigator.native.addRouteRefreshObserver(for: refreshObserver) - navigator.native.startRoutesRefresh( - forDefaultRefreshPeriodMs: UInt64(refreshPeriod * 1000), - ignoreExpirationTime: true - ) - } - - @MainActor - private func subscribeNavigator() { - subscribeElectornicHorizon() - subscribeStatusObserver() - subscribeFallbackObserver() - subscribeContinuousAlternatives() - subscribeRouteRefreshing() - } - - fileprivate func unsubscribeRouteRefreshing() { - guard let navigatorRouteRefreshObserver else { - return - } - navigator.removeRouteRefreshObserver( - for: navigatorRouteRefreshObserver - ) - } - - fileprivate func unsubscribeContinuousAlternatives() { - guard let navigatorAlternativesObserver else { - return - } - navigator.removeRouteAlternativesObserver( - navigatorAlternativesObserver - ) - self.navigatorAlternativesObserver = nil - } - - fileprivate func unsubscribeFallbackObserver() { - navigator.setFallbackVersionsObserverFor( - nil - ) - } - - fileprivate func unsubscribeStatusObserver() { - if let navigatorStatusObserver { - navigator.removeObserver( - for: navigatorStatusObserver - ) - } - } - - private func unsubscribeNavigator() { - stopUpdatingElectronicHorizon(on: navigator) - unsubscribeStatusObserver() - unsubscribeFallbackObserver() - unsubscribeContinuousAlternatives() - unsubscribeRouteRefreshing() - } - - // MARK: - Electronic horizon - - private(set) var roadGraph: RoadGraph - - private(set) var roadObjectStore: RoadObjectStore - - private(set) var roadObjectMatcher: RoadObjectMatcher - - private var isSubscribedToElectronicHorizon = false - - private var electronicHorizonConfig: ElectronicHorizonConfig? { - didSet { - let nativeOptions = electronicHorizonConfig.map(MapboxNavigationNative.ElectronicHorizonOptions.init) - navigator.setElectronicHorizonOptionsFor( - nativeOptions - ) - } - } - - @MainActor - func startUpdatingElectronicHorizon(with config: ElectronicHorizonConfig?) { - startUpdatingElectronicHorizon(with: config, on: navigator) - } - - @MainActor - private func startUpdatingElectronicHorizon( - with config: ElectronicHorizonConfig?, - on navigator: NavigationNativeNavigator - ) { - isSubscribedToElectronicHorizon = true - - let observer = NavigatorElectronicHorizonObserver() - navigatorElectronicHorizonObserver = observer - navigator.native.setElectronicHorizonObserverFor(observer) - electronicHorizonConfig = config - } - - @MainActor - func stopUpdatingElectronicHorizon() { - stopUpdatingElectronicHorizon(on: navigator) - } - - private func stopUpdatingElectronicHorizon(on navigator: NavigationNativeNavigator) { - isSubscribedToElectronicHorizon = false - navigator.setElectronicHorizonObserverFor(nil) - electronicHorizonConfig = nil - } - - // MARK: - Navigator Updates - - @MainActor - func setRoutes( - _ routesData: RoutesData, - uuid: UUID, - legIndex: UInt32, - reason: SetRoutesReason, - completion: @escaping (Result) -> Void - ) { - routeCoordinator.beginActiveNavigation( - with: routesData, - uuid: uuid, - legIndex: legIndex, - reason: reason, - completion: completion - ) - } - - @MainActor - func setAlternativeRoutes( - with routes: [RouteInterface], - completion: @escaping (Result<[RouteAlternative], Error>) -> Void - ) { - routeCoordinator.updateAlternativeRoutes(with: routes, completion: completion) - } - - @MainActor - func updateRouteLeg(to index: UInt32, completion: @escaping (Bool) -> Void) { - let legIndex = UInt32(index) - - navigator.native.changeLeg(forLeg: legIndex, callback: completion) - } - - @MainActor - func unsetRoutes(uuid: UUID, completion: @escaping (Result) -> Void) { - routeCoordinator.endActiveNavigation(with: uuid, completion: completion) - } - - @MainActor - func updateLocation(_ rawLocation: CLLocation, completion: @escaping (Bool) -> Void) { - self.rawLocation = rawLocation - navigator.native.updateLocation(for: FixLocation(rawLocation), callback: completion) - } - - @MainActor - func pause() { - navigator.native.pause() - telemetrySessionManager.reportStopNavigation() - } - - @MainActor - func resume() { - navigator.native.resume() - telemetrySessionManager.reportStartNavigation() - } - - deinit { - unsubscribeNavigator() - if let predictiveCacheManager = configuration.predictiveCacheManager { - Task { @MainActor in - predictiveCacheManager.updateNavigationController(with: nil) - predictiveCacheManager.updateSearchController(with: nil) - } - } - } - - private func subscribeToNotifications() { - _Concurrency.Task { @MainActor in - NotificationCenter.default.addObserver( - self, - selector: #selector(applicationWillTerminate), - name: UIApplication.willTerminateNotification, - object: nil - ) - } - } - - @objc - private func applicationWillTerminate(_ notification: NSNotification) { - telemetrySessionManager.reportStopNavigation() - } -} - -enum NativeNavigatorError: Swift.Error { - case failedToUpdateRoutes(reason: String) - case failedToUpdateAlternativeRoutes(reason: String) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/DefaultRerouteControllerInterface.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/DefaultRerouteControllerInterface.swift deleted file mode 100644 index 147a10ac4..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/DefaultRerouteControllerInterface.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import MapboxNavigationNative_Private - -class DefaultRerouteControllerInterface: RerouteControllerInterface { - typealias RequestConfiguration = (String) -> String - - let nativeInterface: RerouteControllerInterface? - let requestConfig: RequestConfiguration? - - init( - nativeInterface: RerouteControllerInterface?, - requestConfig: RequestConfiguration? = nil - ) { - self.nativeInterface = nativeInterface - self.requestConfig = requestConfig - } - - func reroute(forUrl url: String, callback: @escaping RerouteCallback) { - nativeInterface?.reroute(forUrl: requestConfig?(url) ?? url, callback: callback) - } - - func cancel() { - nativeInterface?.cancel() - } - - func setOptionsAdapterForRouteRequest(_ routeRequestOptionsAdapter: (any RouteOptionsAdapter)?) { - nativeInterface?.setOptionsAdapterForRouteRequest(routeRequestOptionsAdapter) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationNativeNavigator.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationNativeNavigator.swift deleted file mode 100644 index 26a4e07f0..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationNativeNavigator.swift +++ /dev/null @@ -1,143 +0,0 @@ -import Combine -import Foundation -import MapboxCommon -@preconcurrency import MapboxNavigationNative -@preconcurrency import MapboxNavigationNative_Private - -final class NavigationNativeNavigator: @unchecked Sendable { - typealias Completion = @Sendable () -> Void - @MainActor - let native: MapboxNavigationNative.Navigator - var locale: Locale { - didSet { - Task { @MainActor in - updateLocale() - } - } - } - - private var subscriptions: Set = [] - - private func withNavigator(_ callback: @escaping @Sendable (MapboxNavigationNative.Navigator) -> Void) { - Task { @MainActor in - callback(native) - } - } - - @MainActor - init( - navigator: MapboxNavigationNative.Navigator, - locale: Locale - ) { - self.native = navigator - self.locale = locale - - NotificationCenter.default - .publisher(for: NSLocale.currentLocaleDidChangeNotification) - .sink { [weak self] _ in - self?.updateLocale() - } - .store(in: &subscriptions) - } - - @MainActor - private func updateLocale() { - native.config().mutableSettings().setUserLanguagesForLanguages(locale.preferredBCP47Codes) - } - - func removeRouteAlternativesObserver( - _ observer: RouteAlternativesObserver, - completion: Completion? = nil - ) { - withNavigator { - $0.getRouteAlternativesController().removeObserver(for: observer) - completion?() - } - } - - func startNavigationSession(completion: Completion? = nil) { - withNavigator { - $0.startNavigationSession() - completion?() - } - } - - func stopNavigationSession(completion: Completion? = nil) { - withNavigator { - $0.stopNavigationSession() - completion?() - } - } - - func setElectronicHorizonOptionsFor( - _ options: MapboxNavigationNative.ElectronicHorizonOptions?, - completion: Completion? = nil - ) { - withNavigator { - $0.setElectronicHorizonOptionsFor(options) - completion?() - } - } - - func setFallbackVersionsObserverFor( - _ observer: FallbackVersionsObserver?, - completion: Completion? = nil - ) { - withNavigator { - $0.setFallbackVersionsObserverFor(observer) - completion?() - } - } - - func removeObserver( - for observer: NavigatorObserver, - completion: Completion? = nil - ) { - withNavigator { - $0.removeObserver(for: observer) - completion?() - } - } - - func removeRouteRefreshObserver( - for observer: RouteRefreshObserver, - completion: Completion? = nil - ) { - withNavigator { - $0.removeRouteRefreshObserver(for: observer) - completion?() - } - } - - func setElectronicHorizonObserverFor( - _ observer: ElectronicHorizonObserver?, - completion: Completion? = nil - ) { - withNavigator { - $0.setElectronicHorizonObserverFor(observer) - completion?() - } - } - - func setRerouteControllerForController( - _ controller: RerouteControllerInterface, - completion: Completion? = nil - ) { - withNavigator { - $0.setRerouteControllerForController(controller) - completion?() - } - } - - func removeRerouteObserver( - for observer: RerouteObserver, - completion: Completion? = nil - ) { - withNavigator { - $0.removeRerouteObserver(for: observer) - completion?() - } - } -} - -extension MapboxNavigationNative.ElectronicHorizonOptions: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationSessionManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationSessionManager.swift deleted file mode 100644 index fbf31b137..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigationSessionManager.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import MapboxNavigationNative - -protocol NavigationSessionManager { - func reportStartNavigation() - func reportStopNavigation() -} - -final class NavigationSessionManagerImp: NavigationSessionManager { - private let lock: NSLock = .init() - - private var sessionCount: Int - - private let navigator: NavigationNativeNavigator - - init(navigator: NavigationNativeNavigator, previousSession: NavigationSessionManagerImp? = nil) { - self.navigator = navigator - self.sessionCount = previousSession?.sessionCount ?? 0 - } - - func reportStartNavigation() { - var shouldStart = false - lock { - shouldStart = sessionCount == 0 - sessionCount += 1 - } - if shouldStart { - navigator.startNavigationSession() - } - } - - func reportStopNavigation() { - var shouldStop = false - lock { - shouldStop = sessionCount == 1 - sessionCount = max(sessionCount - 1, 0) - } - if shouldStop { - navigator.stopNavigationSession() - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorElectronicHorizonObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorElectronicHorizonObserver.swift deleted file mode 100644 index 4885608d1..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorElectronicHorizonObserver.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import MapboxNavigationNative - -class NavigatorElectronicHorizonObserver: ElectronicHorizonObserver { - public func onPositionUpdated( - for position: ElectronicHorizonPosition, - distances: [MapboxNavigationNative.RoadObjectDistance] - ) { - let positionInfo = RoadGraph.Position(position.position()) - let treeInfo = RoadGraph.Edge(position.tree().start) - let distancesInfo = distances.map(DistancedRoadObject.init) - let updatesMPP = position.type() == .update - - Task { @MainActor in - let userInfo: [RoadGraph.NotificationUserInfoKey: Any] = [ - .positionKey: positionInfo, - .treeKey: treeInfo, - .updatesMostProbablePathKey: updatesMPP, - .distancesByRoadObjectKey: distancesInfo, - ] - - NotificationCenter.default.post( - name: .electronicHorizonDidUpdatePosition, - object: nil, - userInfo: userInfo - ) - } - } - - public func onRoadObjectEnter(for info: RoadObjectEnterExitInfo) { - Task { @MainActor in - let userInfo: [RoadGraph.NotificationUserInfoKey: Any] = [ - .roadObjectIdentifierKey: info.roadObjectId, - .didTransitionAtEndpointKey: info.enterFromStartOrExitFromEnd, - ] - NotificationCenter.default.post(name: .electronicHorizonDidEnterRoadObject, object: nil, userInfo: userInfo) - } - } - - public func onRoadObjectExit(for info: RoadObjectEnterExitInfo) { - Task { @MainActor in - let userInfo: [RoadGraph.NotificationUserInfoKey: Any] = [ - .roadObjectIdentifierKey: info.roadObjectId, - .didTransitionAtEndpointKey: info.enterFromStartOrExitFromEnd, - ] - NotificationCenter.default.post(name: .electronicHorizonDidExitRoadObject, object: nil, userInfo: userInfo) - } - } - - public func onRoadObjectPassed(for info: RoadObjectPassInfo) { - Task { @MainActor in - let userInfo: [RoadGraph.NotificationUserInfoKey: Any] = [ - .roadObjectIdentifierKey: info.roadObjectId, - ] - NotificationCenter.default.post(name: .electronicHorizonDidPassRoadObject, object: nil, userInfo: userInfo) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorFallbackVersionsObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorFallbackVersionsObserver.swift deleted file mode 100644 index 2f816121c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorFallbackVersionsObserver.swift +++ /dev/null @@ -1,66 +0,0 @@ -import Foundation -import MapboxNavigationNative - -class NavigatorFallbackVersionsObserver: FallbackVersionsObserver { - private(set) var tileVersionState: TileVersionState = .nominal - - typealias RestartCallback = (String?) -> Void - let restartCallback: RestartCallback - - init(restartCallback: @escaping RestartCallback) { - self.restartCallback = restartCallback - } - - enum TileVersionState { - /// No tiles version switch is required. Navigator has enough tiles for map matching. - case nominal - /// Navigator does not have tiles on current version for map matching, but TileStore contains regions with - /// required tiles of a different version - case shouldFallback([String]) - /// Navigator is in a fallback mode but newer tiles version were successefully downloaded and ready to use. - case shouldReturnToLatest - } - - func onFallbackVersionsFound(forVersions versions: [String]) { - switch tileVersionState { - case .nominal, .shouldReturnToLatest: - tileVersionState = .shouldFallback(versions) - guard let fallbackVersion = versions.last else { return } - - restartCallback(fallbackVersion) - - let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ - .tilesVersionKey: fallbackVersion, - ] - - NotificationCenter.default.post( - name: .navigationDidSwitchToFallbackVersion, - object: nil, - userInfo: userInfo - ) - case .shouldFallback: - break // do nothing - } - } - - func onCanReturnToLatest(forVersion version: String) { - switch tileVersionState { - case .nominal, .shouldFallback: - tileVersionState = .shouldReturnToLatest - - restartCallback(nil) - - let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ - .tilesVersionKey: version, - ] - - NotificationCenter.default.post( - name: .navigationDidSwitchToTargetVersion, - object: nil, - userInfo: userInfo - ) - case .shouldReturnToLatest: - break // do nothing - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteAlternativesObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteAlternativesObserver.swift deleted file mode 100644 index 6f2963aff..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteAlternativesObserver.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import MapboxNavigationNative - -class NavigatorRouteAlternativesObserver: RouteAlternativesObserver { - func onRouteAlternativesUpdated( - forOnlinePrimaryRoute onlinePrimaryRoute: RouteInterface?, - alternatives: [RouteAlternative], - removedAlternatives: [RouteAlternative] - ) { - // do nothing - } - - func onRouteAlternativesChanged(for routeAlternatives: [RouteAlternative], removed: [RouteAlternative]) { - let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ - .alternativesListKey: routeAlternatives, - .removedAlternativesKey: removed, - ] - NotificationCenter.default.post(name: .navigatorDidChangeAlternativeRoutes, object: nil, userInfo: userInfo) - } - - public func onError(forMessage message: String) { - let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ - .messageKey: message, - ] - NotificationCenter.default.post( - name: .navigatorDidFailToChangeAlternativeRoutes, - object: nil, - userInfo: userInfo - ) - } - - func onOnlinePrimaryRouteAvailable(forOnlinePrimaryRoute onlinePrimaryRoute: RouteInterface) { - let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ - .coincideOnlineRouteKey: onlinePrimaryRoute, - ] - NotificationCenter.default.post( - name: .navigatorWantsSwitchToCoincideOnlineRoute, - object: nil, - userInfo: userInfo - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteRefreshObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteRefreshObserver.swift deleted file mode 100644 index aa09e6f8d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorRouteRefreshObserver.swift +++ /dev/null @@ -1,67 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -@preconcurrency import MapboxNavigationNative - -struct RouteRefreshResult: @unchecked Sendable { - let updatedRoute: RouteInterface - let alternativeRoutes: [RouteAlternative] -} - -class NavigatorRouteRefreshObserver: RouteRefreshObserver, @unchecked Sendable { - typealias RefreshCallback = (String, String, UInt32) async -> RouteRefreshResult? - private var refreshCallback: RefreshCallback - - init(refreshCallback: @escaping RefreshCallback) { - self.refreshCallback = refreshCallback - } - - func onRouteRefreshAnnotationsUpdated( - forRouteId routeId: String, - routeRefreshResponse: String, - routeIndex: UInt32, - legIndex: UInt32, - routeGeometryIndex: UInt32 - ) { - Task { - guard let routeRefreshResult = await self.refreshCallback( - routeRefreshResponse, - "\(routeId)#\(routeIndex)", - routeGeometryIndex - ) else { - return - } - let userInfo: [NativeNavigator.NotificationUserInfoKey: any Sendable] = [ - .refreshRequestIdKey: routeId, - .refreshedRoutesResultKey: routeRefreshResult, - .legIndexKey: legIndex, - ] - - onMainAsync { - NotificationCenter.default.post( - name: .routeRefreshDidUpdateAnnotations, - object: nil, - userInfo: userInfo - ) - } - } - } - - func onRouteRefreshCancelled(forRouteId routeId: String) { - let userInfo: [NativeNavigator.NotificationUserInfoKey: any Sendable] = [ - .refreshRequestIdKey: routeId, - ] - onMainAsync { - NotificationCenter.default.post(name: .routeRefreshDidCancelRefresh, object: nil, userInfo: userInfo) - } - } - - func onRouteRefreshFailed(forRouteId routeId: String, error: RouteRefreshError) { - let userInfo: [NativeNavigator.NotificationUserInfoKey: any Sendable] = [ - .refreshRequestErrorKey: error, - .refreshRequestIdKey: routeId, - ] - onMainAsync { - NotificationCenter.default.post(name: .routeRefreshDidFailRefresh, object: nil, userInfo: userInfo) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorStatusObserver.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorStatusObserver.swift deleted file mode 100644 index 81dab8cb1..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/NavigatorStatusObserver.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import MapboxNavigationNative - -class NavigatorStatusObserver: NavigatorObserver { - var mostRecentNavigationStatus: NavigationStatus? = nil - - func onStatus(for origin: NavigationStatusOrigin, status: NavigationStatus) { - assert(Thread.isMainThread) - - let userInfo: [NativeNavigator.NotificationUserInfoKey: Any] = [ - .originKey: origin, - .statusKey: status, - ] - NotificationCenter.default.post(name: .navigationStatusDidChange, object: nil, userInfo: userInfo) - - mostRecentNavigationStatus = status - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/RerouteController.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/RerouteController.swift deleted file mode 100644 index cfdde64f8..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/RerouteController.swift +++ /dev/null @@ -1,151 +0,0 @@ -import Foundation -import MapboxCommon -import MapboxDirections -import MapboxNavigationNative_Private - -/// Adapter for `MapboxNavigationNative.RerouteControllerInterface` usage inside `Navigator`. -/// -/// This class handles correct setup for `RerouteControllerInterface`, monitoring native reroute events and configuring -/// the process. -class RerouteController { - // MARK: Configuration - - struct Configuration { - let credentials: ApiConfiguration - let navigator: NavigationNativeNavigator - let configHandle: ConfigHandle - let rerouteConfig: RerouteConfig - let initialManeuverAvoidanceRadius: TimeInterval - } - - var initialManeuverAvoidanceRadius: TimeInterval { - get { - config.mutableSettings().avoidManeuverSeconds()?.doubleValue ?? defaultInitialManeuverAvoidanceRadius - } - set { - config.mutableSettings().setAvoidManeuverSecondsForSeconds(NSNumber(value: newValue)) - } - } - - private var config: ConfigHandle - private let rerouteConfig: RerouteConfig - private let defaultInitialManeuverAvoidanceRadius: TimeInterval - var abortReroutePipeline: Bool = false - - // MARK: Reporting Data - - weak var delegate: ReroutingControllerDelegate? - - func userIsOnRoute() -> Bool { - return !(rerouteDetector?.isReroute() ?? false) - } - - // MARK: Internal State Management - - private let defaultRerouteController: DefaultRerouteControllerInterface - private let rerouteDetector: RerouteDetectorInterface? - - private weak var navigator: NavigationNativeNavigator? - - @MainActor - required init(configuration: Configuration) { - self.rerouteConfig = configuration.rerouteConfig - self.navigator = configuration.navigator - self.config = configuration.configHandle - self.defaultInitialManeuverAvoidanceRadius = configuration.initialManeuverAvoidanceRadius - self.defaultRerouteController = DefaultRerouteControllerInterface( - nativeInterface: configuration.navigator.native.getRerouteController() - ) { - guard let url = URL(string: $0), - let options = RouteOptions(url: url) - else { - return $0 - } - - return Directions - .url( - forCalculating: configuration.rerouteConfig.optionsCustomization?(options) ?? options, - credentials: .init(configuration.credentials) - ) - .absoluteString - } - navigator?.native.setRerouteControllerForController(defaultRerouteController) - self.rerouteDetector = configuration.navigator.native.getRerouteDetector() - navigator?.native.addRerouteObserver(for: self) - - defer { - self.initialManeuverAvoidanceRadius = configuration.initialManeuverAvoidanceRadius - } - } - - deinit { - self.navigator?.removeRerouteObserver(for: self) - } -} - -extension RerouteController: RerouteObserver { - func onSwitchToAlternative(forRoute route: any RouteInterface, legIndex: UInt32) { - delegate?.rerouteControllerWantsSwitchToAlternative(self, route: route, legIndex: Int(legIndex)) - } - - func onRerouteDetected(forRouteRequest routeRequest: String) -> Bool { - guard rerouteConfig.detectsReroute else { return false } - delegate?.rerouteControllerDidDetectReroute(self) - return !abortReroutePipeline - } - - func onRerouteReceived(forRouteResponse routeResponse: DataRef, routeRequest: String, origin: RouterOrigin) { - guard rerouteConfig.detectsReroute else { - Log.warning( - "Reroute attempt fetched a route during 'rerouteConfig.detectsReroute' is disabled.", - category: .navigation - ) - return - } - - RouteParser.parseDirectionsResponse( - forResponseDataRef: routeResponse, - request: routeRequest, - routeOrigin: origin - ) { [weak self] result in - guard let self else { return } - - if result.isValue(), - var routes = result.value as? [RouteInterface], - !routes.isEmpty - { - let routesData = RouteParser.createRoutesData( - forPrimaryRoute: routes.remove(at: 0), - alternativeRoutes: routes - ) - delegate?.rerouteControllerDidRecieveReroute(self, routesData: routesData) - } else { - delegate?.rerouteControllerDidFailToReroute(self, with: DirectionsError.invalidResponse(nil)) - } - } - } - - func onRerouteCancelled() { - guard rerouteConfig.detectsReroute else { return } - delegate?.rerouteControllerDidCancelReroute(self) - } - - func onRerouteFailed(forError error: RerouteError) { - guard rerouteConfig.detectsReroute else { - Log.warning( - "Reroute attempt failed with an error during 'rerouteConfig.detectsReroute' is disabled. Error: \(error.message)", - category: .navigation - ) - return - } - delegate?.rerouteControllerDidFailToReroute( - self, - with: DirectionsError.unknown( - response: nil, - underlying: ReroutingError(error), - code: nil, - message: error.message - ) - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/ReroutingControllerDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/ReroutingControllerDelegate.swift deleted file mode 100644 index 6630d83fe..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/CoreNavigator/ReroutingControllerDelegate.swift +++ /dev/null @@ -1,51 +0,0 @@ -import MapboxDirections -import MapboxNavigationNative -import Turf - -protocol ReroutingControllerDelegate: AnyObject { - func rerouteControllerWantsSwitchToAlternative( - _ rerouteController: RerouteController, - route: RouteInterface, - legIndex: Int - ) - func rerouteControllerDidDetectReroute(_ rerouteController: RerouteController) - func rerouteControllerDidRecieveReroute(_ rerouteController: RerouteController, routesData: RoutesData) - func rerouteControllerDidCancelReroute(_ rerouteController: RerouteController) - func rerouteControllerDidFailToReroute(_ rerouteController: RerouteController, with error: DirectionsError) -} - -/// Error type, describing rerouting process malfunction. -public enum ReroutingError: Error { - /// Could not correctly process the reroute. - case routeError - /// Could not compose correct request for rerouting. - case wrongRequest - /// Cause of reroute error is unknown. - case unknown - /// Reroute was cancelled by user. - case cancelled - /// No routes or reroute controller was set to Navigator - case noRoutesOrController - /// Another reroute is in progress. - case anotherRerouteInProgress - - init?(_ nativeError: RerouteError) { - switch nativeError.type { - case .routerError: - self = .routeError - case .unknown: - self = .unknown - case .cancelled: - self = .cancelled - case .noRoutesOrController: - self = .noRoutesOrController - case .buildUriError: - self = .wrongRequest - case .rerouteInProgress: - self = .anotherRerouteInProgress - @unknown default: - assertionFailure("Unknown MapboxNavigationNative.RerouteError value.") - return nil - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/HandlerFactory.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/HandlerFactory.swift deleted file mode 100644 index 36435d047..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/HandlerFactory.swift +++ /dev/null @@ -1,87 +0,0 @@ -import MapboxDirections -import MapboxNavigationNative - -protocol HandlerData { - var tileStorePath: String { get } - var apiConfiguration: ApiConfiguration { get } - var tilesVersion: String { get } - var targetVersion: String? { get } - var configFactoryType: ConfigFactory.Type { get } - var datasetProfileIdentifier: ProfileIdentifier { get } -} - -extension NativeHandlersFactory: HandlerData {} - -/// Creates new or returns existing entity of `HandlerType` constructed with `Arguments`. -/// -/// This factory is required since some of NavNative's handlers are used by multiple unrelated entities and is quite -/// expensive to allocate. Since bindgen-generated `*Factory` classes are not an actual factory but just a wrapper -/// around general init, `HandlerFactory` introduces basic caching of the latest allocated entity. In most of the cases -/// there should never be multiple handlers with different attributes, so such solution is adequate at the moment. -class HandlerFactory { - private struct CacheKey: HandlerData { - let tileStorePath: String - let apiConfiguration: ApiConfiguration - let tilesVersion: String - let targetVersion: String? - let configFactoryType: ConfigFactory.Type - let datasetProfileIdentifier: ProfileIdentifier - - init(data: HandlerData) { - self.tileStorePath = data.tileStorePath - self.apiConfiguration = data.apiConfiguration - self.tilesVersion = data.tilesVersion - self.targetVersion = data.targetVersion - self.configFactoryType = data.configFactoryType - self.datasetProfileIdentifier = data.datasetProfileIdentifier - } - - static func != (lhs: CacheKey, rhs: HandlerData) -> Bool { - return lhs.tileStorePath != rhs.tileStorePath || - lhs.apiConfiguration != rhs.apiConfiguration || - lhs.tilesVersion != rhs.tilesVersion || - lhs.targetVersion != rhs.targetVersion || - lhs.configFactoryType != rhs.configFactoryType || - lhs.datasetProfileIdentifier != rhs.datasetProfileIdentifier - } - } - - typealias BuildHandler = (Arguments) -> HandlerType - let buildHandler: BuildHandler - - private var key: CacheKey? - private var cachedHandle: HandlerType! - private let lock = NSLock() - - fileprivate init(forBuilding buildHandler: @escaping BuildHandler) { - self.buildHandler = buildHandler - } - - func getHandler( - with arguments: Arguments, - cacheData: HandlerData - ) -> HandlerType { - lock.lock(); defer { - lock.unlock() - } - - if key == nil || key! != cacheData { - cachedHandle = buildHandler(arguments) - key = .init(data: cacheData) - } - return cachedHandle - } -} - -let cacheHandlerFactory = HandlerFactory { ( - tilesConfig: TilesConfig, - config: ConfigHandle, - historyRecorder: HistoryRecorderHandle? -) in - CacheFactory.build( - for: tilesConfig, - config: config, - historyRecorder: historyRecorder, - frameworkTypeForSKU: .CF - ) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Movement/NavigationMovementMonitor.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Movement/NavigationMovementMonitor.swift deleted file mode 100644 index ecef4aed7..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Movement/NavigationMovementMonitor.swift +++ /dev/null @@ -1,84 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import MapboxCommon_Private -import MapboxDirections - -final class NavigationMovementMonitor: MovementMonitorInterface { - private var observers: [any MovementModeObserver] { - _observers.read() - } - - var currentProfile: ProfileIdentifier? { - get { - _currentProfile.read() - } - set { - _currentProfile.update(newValue) - notify(with: movementInfo) - } - } - - private let _observers: UnfairLocked<[any MovementModeObserver]> = .init([]) - private let _currentProfile: UnfairLocked = .init(nil) - private let _customMovementInfo: UnfairLocked = .init(nil) - - func getMovementInfo(forCallback callback: @escaping MovementInfoCallback) { - callback(.init(value: movementInfo)) - } - - func setMovementInfoForMode(_ movementInfo: MovementInfo) { - _customMovementInfo.update(movementInfo) - notify(with: movementInfo) - } - - func registerObserver(for observer: any MovementModeObserver) { - _observers.mutate { - $0.append(observer) - } - } - - func unregisterObserver(for observer: any MovementModeObserver) { - _observers.mutate { - $0.removeAll(where: { $0 === observer }) - } - } - - private func notify(with movementInfo: MovementInfo) { - let currentObservers = observers - currentObservers.forEach { - $0.onMovementModeChanged(for: movementInfo) - } - } - - private var movementInfo: MovementInfo { - if let customMovementInfo = _customMovementInfo.read() { - return customMovementInfo - } - let profile = currentProfile - let movementModes: [NSNumber: NSNumber] = if let movementMode = profile?.movementMode { - [100: movementMode.rawValue as NSNumber] - } else if profile != nil { - [50: MovementMode.inVehicle.rawValue as NSNumber] - } else { - [50: MovementMode.unknown.rawValue as NSNumber] - } - return MovementInfo(movementMode: movementModes, movementProvider: .SDK) - } -} - -extension MovementInfo: @unchecked Sendable {} - -extension ProfileIdentifier { - var movementMode: MovementMode? { - switch self { - case .automobile, .automobileAvoidingTraffic: - .inVehicle - case .cycling: - .cycling - case .walking: - .onFoot - default: - nil - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/NativeHandlersFactory.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/NativeHandlersFactory.swift deleted file mode 100644 index 4e56c3312..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/NativeHandlersFactory.swift +++ /dev/null @@ -1,289 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import MapboxCommon -import MapboxCommon_Private -import MapboxDirections -import MapboxNavigationNative -import MapboxNavigationNative_Private - -public let customConfigKey = "com.mapbox.navigation.custom-config" -public let customConfigFeaturesKey = "features" - -/// Internal class, designed for handling initialisation of various NavigationNative entities. -/// -/// Such entities might be used not only as a part of Navigator init sequece, so it is meant not to rely on it's -/// settings. -final class NativeHandlersFactory: @unchecked Sendable { - // MARK: - Settings - - let tileStorePath: String - let apiConfiguration: ApiConfiguration - let tilesVersion: String - let targetVersion: String? - let configFactoryType: ConfigFactory.Type - let datasetProfileIdentifier: ProfileIdentifier - let routingProviderSource: MapboxNavigationNative.RouterType? - - let liveIncidentsOptions: IncidentsConfig? - let navigatorPredictionInterval: TimeInterval? - let statusUpdatingSettings: StatusUpdatingSettings? - let utilizeSensorData: Bool - let historyDirectoryURL: URL? - let initialManeuverAvoidanceRadius: TimeInterval - var locale: Locale { - didSet { - _navigator?.locale = locale - } - } - - init( - tileStorePath: String, - apiConfiguration: ApiConfiguration, - tilesVersion: String, - targetVersion: String? = nil, - configFactoryType: ConfigFactory.Type = ConfigFactory.self, - datasetProfileIdentifier: ProfileIdentifier, - routingProviderSource: MapboxNavigationNative.RouterType? = nil, - liveIncidentsOptions: IncidentsConfig?, - navigatorPredictionInterval: TimeInterval?, - statusUpdatingSettings: StatusUpdatingSettings? = nil, - utilizeSensorData: Bool, - historyDirectoryURL: URL?, - initialManeuverAvoidanceRadius: TimeInterval, - locale: Locale - ) { - self.tileStorePath = tileStorePath - self.apiConfiguration = apiConfiguration - self.tilesVersion = tilesVersion - self.targetVersion = targetVersion - self.configFactoryType = configFactoryType - self.datasetProfileIdentifier = datasetProfileIdentifier - self.routingProviderSource = routingProviderSource - - self.liveIncidentsOptions = liveIncidentsOptions - self.navigatorPredictionInterval = navigatorPredictionInterval - self.statusUpdatingSettings = statusUpdatingSettings - self.utilizeSensorData = utilizeSensorData - self.historyDirectoryURL = historyDirectoryURL - self.initialManeuverAvoidanceRadius = initialManeuverAvoidanceRadius - self.locale = locale - } - - func targeting(version: String?) -> NativeHandlersFactory { - return .init( - tileStorePath: tileStorePath, - apiConfiguration: apiConfiguration, - tilesVersion: tilesVersion, - targetVersion: version, - configFactoryType: configFactoryType, - datasetProfileIdentifier: datasetProfileIdentifier, - routingProviderSource: routingProviderSource, - liveIncidentsOptions: liveIncidentsOptions, - navigatorPredictionInterval: navigatorPredictionInterval, - statusUpdatingSettings: statusUpdatingSettings, - utilizeSensorData: utilizeSensorData, - historyDirectoryURL: historyDirectoryURL, - initialManeuverAvoidanceRadius: initialManeuverAvoidanceRadius, - locale: locale - ) - } - - // MARK: - Native Handlers - - lazy var historyRecorderHandle: HistoryRecorderHandle? = onMainQueueSync { - historyDirectoryURL.flatMap { - HistoryRecorderHandle.build( - forHistoryDir: $0.path, - config: configHandle(by: configFactoryType) - ) - } - } - - private var _navigator: NavigationNativeNavigator? - var navigator: NavigationNativeNavigator { - if let _navigator { - return _navigator - } - return onMainQueueSync { - // Make sure that Navigator pick ups Main Thread RunLoop. - let historyRecorder = historyRecorderHandle - let configHandle = configHandle(by: configFactoryType) - let navigator = if let routingProviderSource { - MapboxNavigationNative.Navigator( - config: configHandle, - cache: cacheHandle, - historyRecorder: historyRecorder, - routerTypeRestriction: routingProviderSource - ) - } else { - MapboxNavigationNative.Navigator( - config: configHandle, - cache: cacheHandle, - historyRecorder: historyRecorder - ) - } - - let nativeNavigator = NavigationNativeNavigator(navigator: navigator, locale: locale) - self._navigator = nativeNavigator - return nativeNavigator - } - } - - lazy var cacheHandle: CacheHandle = cacheHandlerFactory.getHandler( - with: ( - tilesConfig: tilesConfig, - configHandle: configHandle(by: configFactoryType), - historyRecorder: historyRecorderHandle - ), - cacheData: self - ) - - lazy var roadGraph: RoadGraph = .init(MapboxNavigationNative.GraphAccessor(cache: cacheHandle)) - - lazy var tileStore: TileStore = .__create(forPath: tileStorePath) - - // MARK: - Support Objects - - static var settingsProfile: SettingsProfile { - SettingsProfile( - application: .mobile, - platform: .IOS - ) - } - - lazy var endpointConfig: TileEndpointConfiguration = .init( - apiConfiguration: apiConfiguration, - tilesVersion: tilesVersion, - minimumDaysToPersistVersion: nil, - targetVersion: targetVersion, - datasetProfileIdentifier: datasetProfileIdentifier - ) - - lazy var tilesConfig: TilesConfig = .init( - tilesPath: tileStorePath, - tileStore: tileStore, - inMemoryTileCache: nil, - onDiskTileCache: nil, - endpointConfig: endpointConfig, - hdEndpointConfig: nil - ) - - var navigatorConfig: NavigatorConfig { - var nativeIncidentsOptions: MapboxNavigationNative.IncidentsOptions? - if let incidentsOptions = liveIncidentsOptions, - !incidentsOptions.graph.isEmpty - { - nativeIncidentsOptions = .init( - graph: incidentsOptions.graph, - apiUrl: incidentsOptions.apiURL?.absoluteString ?? "" - ) - } - - var pollingConfig: PollingConfig? = nil - - if let predictionInterval = navigatorPredictionInterval { - pollingConfig = PollingConfig( - lookAhead: NSNumber(value: predictionInterval), - unconditionalPatience: nil, - unconditionalInterval: nil - ) - } - if let config = statusUpdatingSettings { - if pollingConfig != nil { - pollingConfig?.unconditionalInterval = config.updatingInterval.map { NSNumber(value: $0) } - pollingConfig?.unconditionalPatience = config.updatingPatience.map { NSNumber(value: $0) } - } else if config.updatingPatience != nil || config.updatingInterval != nil { - pollingConfig = PollingConfig( - lookAhead: nil, - unconditionalPatience: config.updatingPatience - .map { NSNumber(value: $0) }, - unconditionalInterval: config.updatingInterval - .map { NSNumber(value: $0) } - ) - } - } - - return NavigatorConfig( - voiceInstructionThreshold: nil, - electronicHorizonOptions: nil, - polling: pollingConfig, - incidentsOptions: nativeIncidentsOptions, - noSignalSimulationEnabled: nil, - useSensors: NSNumber(booleanLiteral: utilizeSensorData) - ) - } - - func configHandle(by configFactoryType: ConfigFactory.Type = ConfigFactory.self) -> ConfigHandle { - let defaultConfig = [ - customConfigFeaturesKey: [ - "useInternalReroute": true, - "useTelemetryNavigationEvents": true, - ], - "navigation": [ - "alternativeRoutes": [ - "dropDistance": [ - "maxSlightFork": 50.0, - ], - ], - ], - ] - - var customConfig = UserDefaults.standard.dictionary(forKey: customConfigKey) ?? [:] - customConfig.deepMerge(with: defaultConfig, uniquingKeysWith: { first, _ in first }) - - let customConfigJSON: String - if let jsonDataConfig = try? JSONSerialization.data(withJSONObject: customConfig, options: []), - let encodedConfig = String(data: jsonDataConfig, encoding: .utf8) - { - customConfigJSON = encodedConfig - } else { - assertionFailure("Custom config can not be serialized") - customConfigJSON = "" - } - - let configHandle = configFactoryType.build( - for: Self.settingsProfile, - config: navigatorConfig, - customConfig: customConfigJSON - ) - let avoidManeuverSeconds = NSNumber(value: initialManeuverAvoidanceRadius) - configHandle.mutableSettings().setAvoidManeuverSecondsForSeconds(avoidManeuverSeconds) - - configHandle.mutableSettings().setUserLanguagesForLanguages(locale.preferredBCP47Codes) - return configHandle - } - - @MainActor - func telemetry(eventsMetadataProvider: EventsMetadataInterface) -> Telemetry { - navigator.native.getTelemetryForEventsMetadataProvider(eventsMetadataProvider) - } -} - -extension TileEndpointConfiguration { - /// Initializes an object that configures a navigator to obtain routing tiles of the given version from an endpoint, - /// using the given credentials. - /// - Parameters: - /// - apiConfiguration: ApiConfiguration for accessing road network data. - /// - tilesVersion: Routing tile version. - /// - minimumDaysToPersistVersion: The minimum age in days that a tile version much reach before a new version can - /// be requested from the tile endpoint. - /// - targetVersion: Routing tile version, which navigator would like to eventually switch to if it becomes - /// available - /// - datasetProfileIdentifier profile setting, used for selecting tiles type for navigation. - convenience init( - apiConfiguration: ApiConfiguration, - tilesVersion: String, - minimumDaysToPersistVersion: Int?, - targetVersion: String?, - datasetProfileIdentifier: ProfileIdentifier - ) { - self.init( - host: apiConfiguration.endPoint.absoluteString, - dataset: datasetProfileIdentifier.rawValue, - version: tilesVersion, - isFallback: targetVersion != nil, - versionBeforeFallback: targetVersion ?? tilesVersion, - minDiffInDaysToConsiderServerVersion: minimumDaysToPersistVersion as NSNumber? - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/RoutesCoordinator.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/RoutesCoordinator.swift deleted file mode 100644 index f531cc367..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/RoutesCoordinator.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation -import MapboxNavigationNative - -/// Coordinating routes update to NavNative Navigator to rule out some edge scenarios. -final class RoutesCoordinator { - private enum State { - case passiveNavigation - case activeNavigation(UUID) - } - - typealias RoutesResult = (mainRouteInfo: RouteInfo?, alternativeRoutes: [RouteAlternative]) - typealias RoutesSetupHandler = @MainActor ( - _ routesData: RoutesData?, - _ legIndex: UInt32, - _ reason: SetRoutesReason, - _ completion: @escaping (Result) -> Void - ) -> Void - typealias AlternativeRoutesSetupHandler = @MainActor ( - _ routes: [RouteInterface], - _ completion: @escaping (Result<[RouteAlternative], Error>) -> Void - ) -> Void - - private struct ActiveNavigationSession { - let uuid: UUID - } - - private let routesSetupHandler: RoutesSetupHandler - private let alternativeRoutesSetupHandler: AlternativeRoutesSetupHandler - /// The lock that protects mutable state in `RoutesCoordinator`. - private let lock: NSLock - private var state: State - - /// Create a new coordinator that will coordinate requests to set main and alternative routes. - /// - Parameter routesSetupHandler: The handler that passes main and alternative route's`RouteInterface` objects to - /// underlying Navigator. - /// - Parameter alternativeRoutesSetupHandler: The handler that passes only alternative route's`RouteInterface` - /// objects to underlying Navigator. Main route must be set before and it will remain unchanged. - init( - routesSetupHandler: @escaping RoutesSetupHandler, - alternativeRoutesSetupHandler: @escaping AlternativeRoutesSetupHandler - ) { - self.routesSetupHandler = routesSetupHandler - self.alternativeRoutesSetupHandler = alternativeRoutesSetupHandler - self.lock = .init() - self.state = .passiveNavigation - } - - /// - Parameters: - /// - uuid: The UUID of the current active guidances session. All reroutes should have the same uuid. - /// - legIndex: The index of the leg along which to begin navigating. - @MainActor - func beginActiveNavigation( - with routesData: RoutesData, - uuid: UUID, - legIndex: UInt32, - reason: SetRoutesReason, - completion: @escaping (Result) -> Void - ) { - lock.lock() - if case .activeNavigation(let currentUUID) = state, currentUUID != uuid { - Log.fault( - "[BUG] Two simultaneous active navigation sessions. This might happen if there are two NavigationViewController or RouteController instances exists at the same time. Profile the app and make sure that NavigationViewController is deallocated once not in use.", - category: .navigation - ) - } - - state = .activeNavigation(uuid) - lock.unlock() - - routesSetupHandler(routesData, legIndex, reason, completion) - } - - /// - Parameters: - /// - uuid: The UUID that was passed to `RoutesCoordinator.beginActiveNavigation(with:uuid:completion:)` method. - @MainActor - func endActiveNavigation(with uuid: UUID, completion: @escaping (Result) -> Void) { - lock.lock() - guard case .activeNavigation(let currentUUID) = state, currentUUID == uuid else { - lock.unlock() - completion(.failure(RoutesCoordinatorError.endingInvalidActiveNavigation)) - return - } - state = .passiveNavigation - lock.unlock() - routesSetupHandler(nil, 0, .cleanUp, completion) - } - - @MainActor - func updateAlternativeRoutes( - with routes: [RouteInterface], - completion: @escaping (Result<[RouteAlternative], Error>) -> Void - ) { - alternativeRoutesSetupHandler(routes, completion) - } -} - -enum RoutesCoordinatorError: Swift.Error { - /// `RoutesCoordinator.beginActiveNavigation(with:uuid:completion:)` called while the previous navigation wasn't - /// ended with `RoutesCoordinator.endActiveNavigation(with:completion:)` method. - /// - /// It is most likely a sign of a programmer error in the app code. - case endingInvalidActiveNavigation -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/DispatchTimer.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/DispatchTimer.swift deleted file mode 100644 index 2663029fd..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/DispatchTimer.swift +++ /dev/null @@ -1,101 +0,0 @@ -import Dispatch -import Foundation - -/// `DispatchTimer` is a general-purpose wrapper over the `DispatchSourceTimer` mechanism in GCD. -class DispatchTimer { - /// The state of a `DispatchTimer`. - enum State { - /// Timer is active and has an event scheduled. - case armed - /// Timer is idle. - case disarmed - } - - typealias Payload = @Sendable () -> Void - static let defaultAccuracy: DispatchTimeInterval = .milliseconds(500) - - /// Timer current state. - private(set) var state: State = .disarmed - - var countdownInterval: DispatchTimeInterval { - didSet { - reset() - } - } - - private var deadline: DispatchTime { return .now() + countdownInterval } - let repetitionInterval: DispatchTimeInterval - let accuracy: DispatchTimeInterval - let payload: Payload - let timerQueue = DispatchQueue(label: "com.mapbox.SimulatedLocationManager.Timer") - let executionQueue: DispatchQueue - let timer: DispatchSourceTimer - - /// Initializes a new timer. - /// - Parameters: - /// - countdown: The initial time interval for the timer to wait before firing off the payload for the first time. - /// - repetition: The subsequent time interval for the timer to wait before firing off the payload an additional - /// time. Repeats until manually stopped. - /// - accuracy: The amount of leeway, expressed as a time interval, that the timer has in it's timing of the - /// payload execution. Default is 500 milliseconds. - /// - executionQueue: The queue on which the timer executes. Default is main queue. - /// - payload: The payload that executes when the timer expires. - init( - countdown: DispatchTimeInterval, - repeating repetition: DispatchTimeInterval = .never, - accuracy: DispatchTimeInterval = defaultAccuracy, - executingOn executionQueue: DispatchQueue = .main, - payload: @escaping Payload - ) { - self.countdownInterval = countdown - self.repetitionInterval = repetition - self.executionQueue = executionQueue - self.payload = payload - self.accuracy = accuracy - self.timer = DispatchSource.makeTimerSource(flags: [], queue: timerQueue) - } - - deinit { - timer.setEventHandler {} - timer.cancel() - // If the timer is suspended, calling cancel without resuming triggers a crash. This is documented here - // https://forums.developer.apple.com/thread/15902 - if state == .disarmed { - timer.resume() - } - } - - /// Arm the timer. Countdown will begin after this function returns. - func arm() { - guard state == .disarmed, !timer.isCancelled else { return } - state = .armed - scheduleTimer() - timer.setEventHandler { [weak self] in - if let unwrappedSelf = self { - let payload = unwrappedSelf.payload - unwrappedSelf.executionQueue.async(execute: payload) - } - } - timer.resume() - } - - /// Re-arm the timer. Countdown will restart after this function returns. - func reset() { - guard state == .armed, !timer.isCancelled else { return } - timer.suspend() - scheduleTimer() - timer.resume() - } - - /// Disarm the timer. Countdown will stop after this function returns. - func disarm() { - guard state == .armed, !timer.isCancelled else { return } - state = .disarmed - timer.suspend() - timer.setEventHandler {} - } - - private func scheduleTimer() { - timer.schedule(deadline: deadline, repeating: repetitionInterval, leeway: accuracy) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/SimulatedLocationManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/SimulatedLocationManager.swift deleted file mode 100644 index 4607882be..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Internals/Simulation/SimulatedLocationManager.swift +++ /dev/null @@ -1,493 +0,0 @@ -import _MapboxNavigationHelpers -import Combine -import CoreLocation -import Foundation -import MapboxDirections -import Turf - -private let maximumSpeed: CLLocationSpeed = 30 // ~108 kmh -private let minimumSpeed: CLLocationSpeed = 6 // ~21 kmh -private let verticalAccuracy: CLLocationAccuracy = 10 -private let horizontalAccuracy: CLLocationAccuracy = 40 -// minimumSpeed will be used when a location have maximumTurnPenalty -private let maximumTurnPenalty: CLLocationDirection = 90 -// maximumSpeed will be used when a location have minimumTurnPenalty -private let minimumTurnPenalty: CLLocationDirection = 0 -// Go maximum speed if distance to nearest coordinate is >= `safeDistance` -private let safeDistance: CLLocationDistance = 50 - -private class SimulatedLocation: CLLocation, @unchecked Sendable { - var turnPenalty: Double = 0 - - override var description: String { - return "\(super.description) \(turnPenalty)" - } -} - -final class SimulatedLocationManager: NavigationLocationManager, @unchecked Sendable { - @MainActor - init(initialLocation: CLLocation?) { - self.simulatedLocation = initialLocation - - super.init() - - restartTimer() - } - - // MARK: Overrides - - override func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - realLocation = locations.last - } - - // MARK: Specifying Simulation - - private func restartTimer() { - let isArmed = timer?.state == .armed - timer = DispatchTimer( - countdown: .milliseconds(0), - repeating: .milliseconds(updateIntervalMilliseconds / Int(speedMultiplier)), - accuracy: accuracy, - executingOn: queue - ) { [weak self] in - self?.tick() - } - if isArmed { - timer.arm() - } - } - - var speedMultiplier: Double = 1 { - didSet { - restartTimer() - } - } - - override var location: CLLocation? { - get { - simulatedLocation ?? realLocation - } - set { - simulatedLocation = newValue - } - } - - fileprivate var realLocation: CLLocation? - fileprivate var simulatedLocation: CLLocation? - - override var simulatesLocation: Bool { - get { return true } - set { super.simulatesLocation = newValue } - } - - override func startUpdatingLocation() { - timer.arm() - super.startUpdatingLocation() - } - - override func stopUpdatingLocation() { - timer.disarm() - super.stopUpdatingLocation() - } - - // MARK: Simulation Logic - - private var currentDistance: CLLocationDistance = 0 - private var currentSpeed: CLLocationSpeed = 0 - private let accuracy: DispatchTimeInterval = .milliseconds(50) - private let updateIntervalMilliseconds: Int = 1000 - private let defaultTickInterval: TimeInterval = 1 - private var timer: DispatchTimer! - private var locations: [SimulatedLocation]! - private var remainingRouteShape: LineString! - - private let queue = DispatchQueue(label: "com.mapbox.SimulatedLocationManager") - - private(set) var route: Route? - private var routeProgress: RouteProgress? - - private var _nextDate: Date? - private func getNextDate() -> Date { - if _nextDate == nil || _nextDate! < Date() { - _nextDate = Date() - } else { - _nextDate?.addTimeInterval(defaultTickInterval) - } - return _nextDate! - } - - private var slicedIndex: Int? - - private func update(route: Route?) { - // NOTE: this method is expected to be called on the main thread, onMainQueueSync is used as extra check - onMainAsync { [weak self] in - self?.route = route - if let shape = route?.shape { - self?.queue.async { [shape, weak self] in - self?.reset(with: shape) - } - } - } - } - - private func reset(with shape: LineString?) { - guard let shape else { return } - - remainingRouteShape = shape - locations = shape.coordinates.simulatedLocationsWithTurnPenalties() - } - - func tick() { - let ( - expectedSegmentTravelTimes, - originalShape - ) = onMainQueueSync { - ( - routeProgress?.currentLeg.expectedSegmentTravelTimes, - route?.shape - ) - } - - let tickDistance = currentSpeed * defaultTickInterval - guard let remainingShape = remainingRouteShape, - let originalShape, - let indexedNewCoordinate = remainingShape.indexedCoordinateFromStart(distance: tickDistance) - else { - // report last known coordinate or real one - if let simulatedLocation { - self.simulatedLocation = .init(simulatedLocation: simulatedLocation, timestamp: getNextDate()) - } else if #available(iOS 15.0, *), - let realLocation, - let sourceInformation = realLocation.sourceInformation, - sourceInformation.isSimulatedBySoftware - { - // The location is simulated, we need to update timestamp - self.realLocation = .init( - simulatedLocation: realLocation, - timestamp: getNextDate(), - sourceInformation: sourceInformation - ) - } - location.map { locationDelegate?.navigationLocationManager(self, didReceiveNewLocation: $0) } - return - } - if remainingShape.distance() == 0, - let routeDistance = originalShape.distance(), - let lastCoordinate = originalShape.coordinates.last - { - currentDistance = routeDistance - currentSpeed = 0 - - let location = CLLocation( - coordinate: lastCoordinate, - altitude: 0, - horizontalAccuracy: horizontalAccuracy, - verticalAccuracy: verticalAccuracy, - course: 0, - speed: currentSpeed, - timestamp: getNextDate() - ) - onMainQueueSync { [weak self] in - guard let self else { return } - locationDelegate?.navigationLocationManager(self, didReceiveNewLocation: location) - } - - return - } - - let newCoordinate = indexedNewCoordinate.coordinate - // Closest coordinate ahead - guard let lookAheadCoordinate = remainingShape.coordinateFromStart(distance: tickDistance + 10) else { return } - guard let closestCoordinateOnRouteIndex = slicedIndex.map({ idx -> Int? in - originalShape.closestCoordinate( - to: newCoordinate, - startingIndex: idx - )?.index - }) ?? originalShape.closestCoordinate(to: newCoordinate)?.index else { return } - - // Simulate speed based on expected segment travel time - if let expectedSegmentTravelTimes, - let nextCoordinateOnRoute = originalShape.coordinates.after(index: closestCoordinateOnRouteIndex), - let time = expectedSegmentTravelTimes.optional[closestCoordinateOnRouteIndex] - { - let distance = originalShape.coordinates[closestCoordinateOnRouteIndex].distance(to: nextCoordinateOnRoute) - currentSpeed = min(max(distance / time, minimumSpeed), maximumSpeed) - slicedIndex = max(closestCoordinateOnRouteIndex - 1, 0) - } else { - let closestLocation = locations[closestCoordinateOnRouteIndex] - let distanceToClosest = closestLocation.distance(from: CLLocation(newCoordinate)) - let distance = min(max(distanceToClosest, 10), safeDistance) - let coordinatesNearby = remainingShape.trimmed(from: newCoordinate, distance: 100)!.coordinates - currentSpeed = calculateCurrentSpeed( - distance: distance, - coordinatesNearby: coordinatesNearby, - closestLocation: closestLocation - ) - } - - let location = CLLocation( - coordinate: newCoordinate, - altitude: 0, - horizontalAccuracy: horizontalAccuracy, - verticalAccuracy: verticalAccuracy, - course: newCoordinate.direction(to: lookAheadCoordinate).wrap(min: 0, max: 360), - speed: currentSpeed, - timestamp: getNextDate() - ) - - simulatedLocation = location - - onMainQueueSync { - locationDelegate?.navigationLocationManager( - self, - didReceiveNewLocation: location - ) - } - currentDistance += remainingShape.distance(to: newCoordinate) ?? 0 - remainingRouteShape = remainingShape.sliced(from: newCoordinate) - } - - func progressDidChange(_ progress: RouteProgress?) { - guard let progress else { - cleanUp() - return - } - onMainQueueSync { - self.routeProgress = progress - if progress.route.distance != self.route?.distance { - update(route: progress.route) - } - } - } - - func cleanUp() { - route = nil - routeProgress = nil - remainingRouteShape = nil - locations = [] - } - - func didReroute(progress: RouteProgress?) { - guard let progress else { return } - - update(route: progress.route) - - let shape = progress.route.shape - let currentSpeed = currentSpeed - - queue.async { [weak self] in - guard let self, - let routeProgress else { return } - - var newClosestCoordinate: LocationCoordinate2D! - if let location, - let shape, - let closestCoordinate = shape.closestCoordinate(to: location.coordinate) - { - simulatedLocation = location - currentDistance = closestCoordinate.distance - newClosestCoordinate = closestCoordinate.coordinate - } else { - currentDistance = calculateCurrentDistance(routeProgress.distanceTraveled, speed: currentSpeed) - newClosestCoordinate = shape?.coordinateFromStart(distance: currentDistance) - } - - onMainQueueSync { - self.routeProgress = progress - self.route = progress.route - } - reset(with: shape) - remainingRouteShape = remainingRouteShape.sliced(from: newClosestCoordinate) - slicedIndex = nil - } - } -} - -// MARK: - Helpers - -extension Double { - fileprivate func scale(minimumIn: Double, maximumIn: Double, minimumOut: Double, maximumOut: Double) -> Double { - return ((maximumOut - minimumOut) * (self - minimumIn) / (maximumIn - minimumIn)) + minimumOut - } -} - -extension CLLocation { - fileprivate convenience init(_ coordinate: CLLocationCoordinate2D) { - self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) - } - - fileprivate convenience init( - simulatedLocation: CLLocation, - timestamp: Date - ) { - self.init( - coordinate: simulatedLocation.coordinate, - altitude: simulatedLocation.altitude, - horizontalAccuracy: simulatedLocation.horizontalAccuracy, - verticalAccuracy: simulatedLocation.verticalAccuracy, - course: simulatedLocation.course, - speed: simulatedLocation.speed, - timestamp: timestamp - ) - } - - @available(iOS 15.0, *) - fileprivate convenience init( - simulatedLocation: CLLocation, - timestamp: Date, - sourceInformation: CLLocationSourceInformation - ) { - self.init( - coordinate: simulatedLocation.coordinate, - altitude: simulatedLocation.altitude, - horizontalAccuracy: simulatedLocation.horizontalAccuracy, - verticalAccuracy: simulatedLocation.verticalAccuracy, - course: simulatedLocation.course, - courseAccuracy: simulatedLocation.courseAccuracy, - speed: simulatedLocation.speed, - speedAccuracy: simulatedLocation.speedAccuracy, - timestamp: timestamp, - sourceInfo: sourceInformation - ) - } -} - -extension Array where Element: Hashable { - fileprivate struct OptionalSubscript { - var elements: [Element] - subscript(index: Int) -> Element? { - return index < elements.count ? elements[index] : nil - } - } - - fileprivate var optional: OptionalSubscript { return OptionalSubscript(elements: self) } -} - -extension Array where Element: Equatable { - fileprivate func after(index: Index) -> Element? { - if index + 1 < count { - return self[index + 1] - } - return nil - } -} - -extension [CLLocationCoordinate2D] { - // Calculate turn penalty for each coordinate. - fileprivate func simulatedLocationsWithTurnPenalties() -> [SimulatedLocation] { - var locations = [SimulatedLocation]() - - for (coordinate, nextCoordinate) in zip(prefix(upTo: endIndex - 1), suffix(from: 1)) { - let currentCoordinate = locations.isEmpty ? first! : coordinate - let course = coordinate.direction(to: nextCoordinate).wrap(min: 0, max: 360) - let turnPenalty = currentCoordinate.direction(to: coordinate) - .difference(from: coordinate.direction(to: nextCoordinate)) - let location = SimulatedLocation( - coordinate: coordinate, - altitude: 0, - horizontalAccuracy: horizontalAccuracy, - verticalAccuracy: verticalAccuracy, - course: course, - speed: minimumSpeed, - timestamp: Date() - ) - location.turnPenalty = Swift.max(Swift.min(turnPenalty, maximumTurnPenalty), minimumTurnPenalty) - locations.append(location) - } - - locations.append(SimulatedLocation( - coordinate: last!, - altitude: 0, - horizontalAccuracy: horizontalAccuracy, - verticalAccuracy: verticalAccuracy, - course: locations.last!.course, - speed: minimumSpeed, - timestamp: Date() - )) - - return locations - } -} - -extension LineString { - fileprivate typealias DistanceIndex = (distance: LocationDistance, index: Int) - - fileprivate func closestCoordinate(to coordinate: LocationCoordinate2D, startingIndex: Int) -> DistanceIndex? { - // Ported from https://github.com/Turfjs/turf/blob/142e137ce0c758e2825a260ab32b24db0aa19439/packages/turf-point-on-line/index.js - guard let startCoordinate = coordinates.first, - coordinates.indices.contains(startingIndex) else { return nil } - - guard coordinates.count > 1 else { - return (coordinate.distance(to: startCoordinate), 0) - } - - var closestCoordinate: DistanceIndex? - var closestDistance: LocationDistance? - - for index in startingIndex.. CLLocationDistance { - return distance + speed -} - -private func calculateCurrentSpeed( - distance: CLLocationDistance, - coordinatesNearby: [CLLocationCoordinate2D]? = nil, - closestLocation: SimulatedLocation -) -> CLLocationSpeed { - // More than 10 nearby coordinates indicates that we are in a roundabout or similar complex shape. - if let coordinatesNearby, coordinatesNearby.count >= 10 { - return minimumSpeed - } - // Maximum speed if we are a safe distance from the closest coordinate - else if distance >= safeDistance { - return maximumSpeed - } - // Base speed on previous or upcoming turn penalty - else { - let reversedTurnPenalty = maximumTurnPenalty - closestLocation.turnPenalty - return reversedTurnPenalty.scale( - minimumIn: minimumTurnPenalty, - maximumIn: maximumTurnPenalty, - minimumOut: minimumSpeed, - maximumOut: maximumSpeed - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationClient.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationClient.swift deleted file mode 100644 index 8e104d3c2..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationClient.swift +++ /dev/null @@ -1,96 +0,0 @@ -import Combine -import CoreLocation - -public struct LocationClient: @unchecked Sendable, Equatable { - var locations: AnyPublisher - var headings: AnyPublisher - var startUpdatingLocation: @MainActor () -> Void - var stopUpdatingLocation: @MainActor () -> Void - var startUpdatingHeading: @MainActor () -> Void - var stopUpdatingHeading: @MainActor () -> Void - - public init( - locations: AnyPublisher, - headings: AnyPublisher, - startUpdatingLocation: @escaping () -> Void, - stopUpdatingLocation: @escaping () -> Void, - startUpdatingHeading: @escaping () -> Void, - stopUpdatingHeading: @escaping () -> Void - ) { - self.locations = locations - self.headings = headings - self.startUpdatingLocation = startUpdatingLocation - self.stopUpdatingLocation = stopUpdatingLocation - self.startUpdatingHeading = startUpdatingHeading - self.stopUpdatingHeading = stopUpdatingHeading - } - - private let id = UUID().uuidString - public static func == (lhs: LocationClient, rhs: LocationClient) -> Bool { lhs.id == rhs.id } -} - -extension LocationClient { - static var liveValue: Self { - class Delegate: NSObject, CLLocationManagerDelegate { - var locations: AnyPublisher { - locationsSubject.eraseToAnyPublisher() - } - - var headings: AnyPublisher { - headingSubject.eraseToAnyPublisher() - } - - private let manager = CLLocationManager() - private let locationsSubject = PassthroughSubject() - private let headingSubject = PassthroughSubject() - - override init() { - super.init() - assert(Thread.isMainThread) // CLLocationManager has to be created on the main thread - manager.requestWhenInUseAuthorization() - - if Bundle.main.backgroundModes.contains("location") { - manager.allowsBackgroundLocationUpdates = true - } - manager.delegate = self - } - - func startUpdatingLocation() { - manager.startUpdatingLocation() - } - - func stopUpdatingLocation() { - manager.stopUpdatingLocation() - } - - func startUpdatingHeading() { - manager.startUpdatingHeading() - } - - func stopUpdatingHeading() { - manager.stopUpdatingHeading() - } - - nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - if let location = locations.last { - locationsSubject.send(location) - } - } - - nonisolated func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { - headingSubject.send(newHeading) - } - } - - let delegate = Delegate() - - return Self( - locations: delegate.locations, - headings: delegate.headings, - startUpdatingLocation: { delegate.startUpdatingLocation() }, - stopUpdatingLocation: { delegate.stopUpdatingLocation() }, - startUpdatingHeading: { delegate.startUpdatingHeading() }, - stopUpdatingHeading: { delegate.stopUpdatingHeading() } - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationSource.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationSource.swift deleted file mode 100644 index 038536262..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/LocationSource.swift +++ /dev/null @@ -1,8 +0,0 @@ -import CoreLocation -import Foundation - -public enum LocationSource: Equatable, @unchecked Sendable { - case simulation(initialLocation: CLLocation? = nil) - case live - case custom(LocationClient) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/MultiplexLocationClient.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/MultiplexLocationClient.swift deleted file mode 100644 index 5747b63bf..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/MultiplexLocationClient.swift +++ /dev/null @@ -1,139 +0,0 @@ -import Combine -import CoreLocation -import Foundation - -/// Allows switching between sources of location data: -/// ``LocationSource/live`` which sends real GPS locations; -/// ``LocationSource/simulation(initialLocation:)`` that simulates the route traversal; -/// ``LocationSource/custom(_:)`` that allows to provide a custom location data source; -@MainActor -public final class MultiplexLocationClient: @unchecked Sendable { - private let locations = PassthroughSubject() - private let headings = PassthroughSubject() - private var currentLocationClient: LocationClient = .empty - private var isUpdating = false - private var isUpdatingHeading = false - private var routeProgress: AnyPublisher = Just(nil).eraseToAnyPublisher() - private var rerouteEvents: AnyPublisher = Just(nil).eraseToAnyPublisher() - private var currentLocationClientSubscriptions: Set = [] - - var locationClient: LocationClient { - .init( - locations: locations.eraseToAnyPublisher(), - headings: headings.eraseToAnyPublisher(), - startUpdatingLocation: { [weak self] in self?.startUpdatingLocation() }, - stopUpdatingLocation: { [weak self] in self?.stopUpdatingLocation() }, - startUpdatingHeading: { [weak self] in self?.startUpdatingHeading() }, - stopUpdatingHeading: { [weak self] in self?.stopUpdatingHeading() } - ) - } - - var isInitialized: Bool = false - - nonisolated init(source: LocationSource) { - setLocationSource(source) - } - - nonisolated func subscribeToNavigatorUpdates( - _ navigator: MapboxNavigator, - source: LocationSource - ) { - Task { @MainActor in - self.isInitialized = true - self.routeProgress = navigator.routeProgress - self.rerouteEvents = navigator.navigationRoutes - .map { _ in navigator.currentRouteProgress?.routeProgress } - .eraseToAnyPublisher() - setLocationSource(source) - } - } - - func startUpdatingLocation() { - isUpdating = true - Task { @MainActor in - currentLocationClient.startUpdatingLocation() - } - } - - func stopUpdatingLocation() { - isUpdating = false - Task { @MainActor in - currentLocationClient.stopUpdatingLocation() - } - } - - func startUpdatingHeading() { - isUpdatingHeading = true - Task { @MainActor in - currentLocationClient.startUpdatingHeading() - } - } - - func stopUpdatingHeading() { - isUpdatingHeading = false - Task { @MainActor in - currentLocationClient.stopUpdatingHeading() - } - } - - nonisolated func setLocationSource(_ source: LocationSource) { - Task { @MainActor in - let newLocationClient: LocationClient - switch source { - case .simulation(let location): - newLocationClient = .simulatedLocationManager( - routeProgress: routeProgress, - rerouteEvents: rerouteEvents, - initialLocation: location - ) - if let location { - locations.send(location) - } - case .live: - newLocationClient = .liveValue - case .custom(let customClient): - newLocationClient = customClient - } - - currentLocationClient.stopUpdatingHeading() - currentLocationClient.stopUpdatingLocation() - - if isUpdating { - newLocationClient.startUpdatingLocation() - } else { - newLocationClient.stopUpdatingLocation() - } - - if isUpdatingHeading { - newLocationClient.startUpdatingHeading() - } else { - newLocationClient.stopUpdatingHeading() - } - - currentLocationClient = newLocationClient - currentLocationClientSubscriptions.removeAll() - - newLocationClient.locations - .subscribe(on: DispatchQueue.main) - .sink { [weak self] in - self?.locations.send($0) - } - .store(in: ¤tLocationClientSubscriptions) - newLocationClient.headings - .subscribe(on: DispatchQueue.main) - .sink { [weak self] in self?.headings.send($0) } - .store(in: ¤tLocationClientSubscriptions) - } - } -} - -extension LocationClient { - fileprivate static let empty = LocationClient( - locations: Empty().eraseToAnyPublisher(), - headings: Empty().eraseToAnyPublisher(), - startUpdatingLocation: {}, - stopUpdatingLocation: {}, - startUpdatingHeading: {}, - stopUpdatingHeading: {} - ) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/SimulatedLocationManagerWrapper.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/SimulatedLocationManagerWrapper.swift deleted file mode 100644 index 76c3b9924..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/LocationClient/SimulatedLocationManagerWrapper.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Combine -import CoreLocation - -extension LocationClient { - @MainActor - static func simulatedLocationManager( - routeProgress: AnyPublisher, - rerouteEvents: AnyPublisher, - initialLocation: CLLocation? - ) -> Self { - let wrapper = SimulatedLocationManagerWrapper( - routeProgress: routeProgress, - rerouteEvents: rerouteEvents, - initialLocation: initialLocation - ) - return Self( - locations: wrapper.locations, - headings: Empty().eraseToAnyPublisher(), - startUpdatingLocation: { - wrapper.startUpdatingLocation() - }, - stopUpdatingLocation: { - wrapper.stopUpdatingLocation() - }, - startUpdatingHeading: {}, - stopUpdatingHeading: {} - ) - } -} - -@MainActor -private class SimulatedLocationManagerWrapper: NavigationLocationManagerDelegate { - private let manager: SimulatedLocationManager - private let _locations = PassthroughSubject() - private var lifetimeSubscriptions: Set = [] - - var locations: AnyPublisher { _locations.eraseToAnyPublisher() } - - @MainActor - init( - routeProgress: AnyPublisher, - rerouteEvents: AnyPublisher, - initialLocation: CLLocation? - ) { - self.manager = SimulatedLocationManager(initialLocation: initialLocation) - manager.locationDelegate = self - - routeProgress.sink { [weak self] in - self?.manager.progressDidChange($0?.routeProgress) - }.store(in: &lifetimeSubscriptions) - - rerouteEvents.sink { [weak self] in - self?.manager.didReroute(progress: $0) - }.store(in: &lifetimeSubscriptions) - } - - func startUpdatingLocation() { - manager.startUpdatingLocation() - } - - func stopUpdatingLocation() { - manager.stopUpdatingLocation() - } - - nonisolated func navigationLocationManager( - _ locationManager: NavigationLocationManager, - didReceiveNewLocation location: CLLocation - ) { - Task { @MainActor in - self._locations.send(location) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapMatchingResult.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapMatchingResult.swift deleted file mode 100644 index d68df8bfa..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapMatchingResult.swift +++ /dev/null @@ -1,79 +0,0 @@ -import CoreLocation -import Foundation -import MapboxNavigationNative - -/// Provides information about the status of the enhanced location updates generated by the map matching engine of the -/// Navigation SDK. -public struct MapMatchingResult: Equatable, @unchecked Sendable { - /// The best possible location update, snapped to the route or map matched to the road if possible - public var enhancedLocation: CLLocation - - /// A list of predicted location points leading up to the target update. - /// - /// The last point on the list (if it is not empty) is always equal to `enhancedLocation`. - public var keyPoints: [CLLocation] - - /// Whether the SDK thinks that the user is off road. - /// - /// Based on the `offRoadProbability`. - public var isOffRoad: Bool - - /// Probability that the user is off road. - public var offRoadProbability: Double - - /// Returns true if map matcher changed its opinion about most probable path on last update. - /// - /// In practice it means we don't need to animate puck movement from previous to current location and just do an - /// immediate transition instead. - public var isTeleport: Bool - - /// When map matcher snaps to a road, this is the confidence in the chosen edge from all nearest edges. - public var roadEdgeMatchProbability: Double - - /// Creates a new `MapMatchingResult` with given parameters - /// - /// It is not intended for user to create his own `MapMatchingResult` except for testing purposes. - @_documentation(visibility: internal) - public init( - enhancedLocation: CLLocation, - keyPoints: [CLLocation], - isOffRoad: Bool, - offRoadProbability: Double, - isTeleport: Bool, - roadEdgeMatchProbability: Double - ) { - self.enhancedLocation = enhancedLocation - self.keyPoints = keyPoints - self.isOffRoad = isOffRoad - self.offRoadProbability = offRoadProbability - self.isTeleport = isTeleport - self.roadEdgeMatchProbability = roadEdgeMatchProbability - } - - init(status: NavigationStatus) { - self.enhancedLocation = CLLocation(status.location) - self.keyPoints = status.keyPoints.map { CLLocation($0) } - self.isOffRoad = status.offRoadProba > 0.5 - self.offRoadProbability = Double(status.offRoadProba) - self.isTeleport = status.mapMatcherOutput.isTeleport - self.roadEdgeMatchProbability = Double(status.mapMatcherOutput.matches.first?.proba ?? 0.0) - } -} - -extension CLLocation { - convenience init(_ location: FixLocation) { - let timestamp = Date(timeIntervalSince1970: TimeInterval(location.monotonicTimestampNanoseconds) / 1e9) - self.init( - coordinate: location.coordinate, - altitude: location.altitude?.doubleValue ?? 0, - horizontalAccuracy: location.accuracyHorizontal?.doubleValue ?? -1, - verticalAccuracy: location.verticalAccuracy?.doubleValue ?? -1, - course: location.bearing?.doubleValue ?? -1, - courseAccuracy: location.bearingAccuracy?.doubleValue ?? -1, - // TODO: investigate why we need 0 when in v2 we used to have -1 - speed: location.speed?.doubleValue ?? 0, - speedAccuracy: location.speedAccuracy?.doubleValue ?? -1, - timestamp: timestamp - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapboxNavigator.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapboxNavigator.swift deleted file mode 100644 index f2ecce96a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/MapboxNavigator.swift +++ /dev/null @@ -1,1414 +0,0 @@ -import _MapboxNavigationHelpers -import Combine -import Foundation -import MapboxDirections -@preconcurrency import MapboxNavigationNative - -final class MapboxNavigator: @unchecked Sendable { - struct Configuration: @unchecked Sendable { - let navigator: CoreNavigator - let routeParserType: RouteParser.Type - let locationClient: LocationClient - let alternativesAcceptionPolicy: AlternativeRoutesDetectionConfig.AcceptionPolicy? - let billingHandler: BillingHandler - let multilegAdvancing: CoreConfig.MultilegAdvanceMode - let prefersOnlineRoute: Bool - let disableBackgroundTrackingLocation: Bool - let fasterRouteController: FasterRouteProvider? - let electronicHorizonConfig: ElectronicHorizonConfig? - let congestionConfig: CongestionRangesConfiguration - let movementMonitor: NavigationMovementMonitor - } - - // MARK: - Navigator Implementation - - @CurrentValuePublisher var session: AnyPublisher - @MainActor - var currentSession: Session { - _session.value - } - - @CurrentValuePublisher var routeProgress: AnyPublisher - var currentRouteProgress: RouteProgressState? { - _routeProgress.value - } - - @CurrentValuePublisher var mapMatching: AnyPublisher - @MainActor - var currentMapMatching: MapMatchingState? { - _mapMatching.value - } - - @EventPublisher var offlineFallbacks - - @EventPublisher var voiceInstructions - - @EventPublisher var bannerInstructions - - @EventPublisher var waypointsArrival - - @EventPublisher var rerouting - - @EventPublisher var continuousAlternatives - - @EventPublisher var fasterRoutes - - @EventPublisher var routeRefreshing - - @EventPublisher var eHorizonEvents - - @EventPublisher var errors - - var heading: AnyPublisher { - locationClient.headings - } - - @CurrentValuePublisher var navigationRoutes: AnyPublisher - var currentNavigationRoutes: NavigationRoutes? { - _navigationRoutes.value - } - - let roadMatching: RoadMatching - - @MainActor - func startActiveGuidance(with navigationRoutes: NavigationRoutes, startLegIndex: Int) { - send(navigationRoutes) - Task { - await updateRouteProgress(with: navigationRoutes) - } - taskManager.withBarrier { - setRoutes( - navigationRoutes: navigationRoutes, - startLegIndex: startLegIndex, - reason: .newRoute - ) - } - let profile = navigationRoutes.mainRoute.route.legs.first?.profileIdentifier - configuration.movementMonitor.currentProfile = profile - } - - private let statusUpdateEvents: AsyncStreamBridge - - enum SetRouteReason { - case newRoute - case reroute - case alternatives - case fasterRoute - case fallbackToOffline - case restoreToOnline - } - - private var setRoutesTask: Task? - - @MainActor - func setRoutes(navigationRoutes: NavigationRoutes, startLegIndex: Int, reason: SetRouteReason) { - verifyActiveGuidanceBillingSession(for: navigationRoutes) - - guard let sessionUUID else { - Log.error( - "Failed to set routes due to missing session ID.", - category: .billing - ) - send(NavigatorErrors.FailedToSetRoute(underlyingError: nil)) - return - } - - locationClient.startUpdatingLocation() - locationClient.startUpdatingHeading() - navigator.resume() - - navigator.setRoutes( - navigationRoutes.asRoutesData(), - uuid: sessionUUID, - legIndex: UInt32(startLegIndex), - reason: reason.navNativeValue - ) { [weak self] result in - guard let self else { return } - - setRoutesTask?.cancel() - setRoutesTask = Task.detached { - switch result { - case .success(let info): - var navigationRoutes = navigationRoutes - let alternativeRoutes = await AlternativeRoute.fromNative( - alternativeRoutes: info.alternativeRoutes, - relateveTo: navigationRoutes.mainRoute - ) - - guard !Task.isCancelled else { return } - navigationRoutes.allAlternativeRoutesWithIgnored = alternativeRoutes - await self.updateRouteProgress(with: navigationRoutes) - await self.send(navigationRoutes) - switch reason { - case .newRoute: - // Do nothing, routes updates are already sent - break - case .reroute: - await self.send( - ReroutingStatus(event: ReroutingStatus.Events.Fetched()) - ) - case .alternatives: - let event = AlternativesStatus.Events.SwitchedToAlternative(navigationRoutes: navigationRoutes) - await self.send(AlternativesStatus(event: event)) - case .fasterRoute: - await self.send(FasterRoutesStatus(event: FasterRoutesStatus.Events.Applied())) - case .fallbackToOffline: - await self.send( - FallbackToTilesState(usingLatestTiles: false) - ) - case .restoreToOnline: - await self.send(FallbackToTilesState(usingLatestTiles: true)) - } - await self.send(Session(state: .activeGuidance(.uncertain))) - case .failure(let error): - Log.error("Failed to set routes, error: \(error).", category: .navigation) - await self.send(NavigatorErrors.FailedToSetRoute(underlyingError: error)) - } - self.setRoutesTask = nil - self.rerouteController?.abortReroutePipeline = navigationRoutes.isCustomExternalRoute - } - } - } - - func selectAlternativeRoute(at index: Int) { - taskManager.cancellableTask { [self] in - guard case .activeGuidance = await currentSession.state, - let alternativeRoutes = await currentNavigationRoutes?.selectingAlternativeRoute(at: index), - !Task.isCancelled - else { - Log.warning( - "Attempt to select invalid alternative route (index '\(index)' of alternatives - '\(String(describing: currentNavigationRoutes))').", - category: .navigation - ) - await send(NavigatorErrors.FailedToSelectAlternativeRoute()) - return - } - - await setRoutes( - navigationRoutes: alternativeRoutes, - startLegIndex: 0, - reason: .alternatives - ) - } - } - - func selectAlternativeRoute(with routeId: RouteId) { - guard let index = currentNavigationRoutes?.alternativeRoutes.firstIndex(where: { $0.routeId == routeId }) else { - Log.warning( - "Attempt to select invalid alternative route with '\(routeId)' available ids - '\((currentNavigationRoutes?.alternativeRoutes ?? []).map(\.routeId))'", - category: .navigation - ); return - } - - selectAlternativeRoute(at: index) - } - - func switchLeg(newLegIndex: Int) { - taskManager.cancellableTask { @MainActor [self] in - guard case .activeGuidance = currentSession.state, - billingSessionIsActive(withType: .activeGuidance), - !Task.isCancelled - else { - Log.warning("Attempt to switch route leg while not in Active Guidance.", category: .navigation) - return - } - - navigator.updateRouteLeg(to: UInt32(newLegIndex)) { [weak self] success in - Task { [weak self] in - if success { - guard let sessionUUID = self?.sessionUUID else { - Log.error( - "Route leg switching failed due to missing session ID.", - category: .billing - ) - await self?.send(NavigatorErrors.FailedToSelectRouteLeg()) - return - } - self?.billingHandler.beginNewBillingSessionIfExists(with: sessionUUID) - let event = WaypointArrivalStatus.Events.NextLegStarted(newLegIndex: newLegIndex) - await self?.send(WaypointArrivalStatus(event: event)) - } else { - Log.warning("Route leg switching failed.", category: .navigation) - await self?.send(NavigatorErrors.FailedToSelectRouteLeg()) - } - } - } - } - } - - @MainActor - func setToIdle() { - taskManager.withBarrier { - let hadActiveGuidance = billingSessionIsActive(withType: .activeGuidance) - if let sessionUUID, - billingSessionIsActive() - { - billingHandler.pauseBillingSession(with: sessionUUID) - } - - guard currentSession.state != .idle else { - Log.warning("Duplicate setting to idle state attempted", category: .navigation) - send(NavigatorErrors.FailedToSetToIdle()) - return - } - - send(NavigationRoutes?.none) - send(RouteProgressState?.none) - locationClient.stopUpdatingLocation() - locationClient.stopUpdatingHeading() - navigator.pause() - - guard hadActiveGuidance else { - send(Session(state: .idle)) - return - } - guard let sessionUUID = self.sessionUUID else { - Log.error( - "`MapboxNavigator.setToIdle` failed to reset routes due to missing session ID.", - category: .billing - ) - send(NavigatorErrors.FailedToSetToIdle()) - return - } - - navigator.unsetRoutes(uuid: sessionUUID) { result in - Task { - if case .failure(let error) = result { - Log.warning( - "`MapboxNavigator.setToIdle` failed to reset routes with error: \(error)", - category: .navigation - ) - } - await self.send(Session(state: .idle)) - } - } - billingHandler.stopBillingSession(with: sessionUUID) - self.sessionUUID = nil - } - configuration.movementMonitor.currentProfile = nil - } - - @MainActor - func startFreeDrive() { - taskManager.withBarrier { - let activeGuidanceSession = verifyFreeDriveBillingSession() - - guard sessionUUID != nil else { - Log.error( - "`MapboxNavigator.startFreeDrive` failed to start new session due to missing session ID.", - category: .billing - ) - return - } - - send(NavigationRoutes?.none) - send(RouteProgressState?.none) - locationClient.startUpdatingLocation() - locationClient.startUpdatingHeading() - navigator.resume() - if let activeGuidanceSession { - navigator.unsetRoutes(uuid: activeGuidanceSession) { result in - Task { - if case .failure(let error) = result { - Log.warning( - "`MapboxNavigator.startFreeDrive` failed to reset routes with error: \(error)", - category: .navigation - ) - } - await self.send(Session(state: .freeDrive(.active))) - } - } - } else { - send(Session(state: .freeDrive(.active))) - } - } - } - - @MainActor - func pauseFreeDrive() { - taskManager.withBarrier { - guard case .freeDrive = currentSession.state, - let sessionUUID, - billingSessionIsActive(withType: .freeDrive) - else { - send(NavigatorErrors.FailedToPause()) - Log.warning( - "Attempt to pause navigation while not in Free Drive.", - category: .navigation - ) - return - } - locationClient.stopUpdatingLocation() - locationClient.stopUpdatingHeading() - navigator.pause() - billingHandler.pauseBillingSession(with: sessionUUID) - send(Session(state: .freeDrive(.paused))) - } - } - - func startUpdatingEHorizon() { - guard let config = configuration.electronicHorizonConfig else { - return - } - - Task { @MainActor in - navigator.startUpdatingElectronicHorizon(with: config) - } - } - - func stopUpdatingEHorizon() { - Task { @MainActor in - navigator.stopUpdatingElectronicHorizon() - } - } - - // MARK: - Billing checks - - @MainActor - private func billingSessionIsActive(withType type: BillingHandler.SessionType? = nil) -> Bool { - guard let sessionUUID, - billingHandler.sessionState(uuid: sessionUUID) == .running - else { - return false - } - - if let type, - billingHandler.sessionType(uuid: sessionUUID) != type - { - return false - } - - return true - } - - @MainActor - private func beginNewSession(of type: BillingHandler.SessionType) { - let newSession = UUID() - sessionUUID = newSession - billingHandler.beginBillingSession( - for: type, - uuid: newSession - ) - } - - @MainActor - private func verifyActiveGuidanceBillingSession(for navigationRoutes: NavigationRoutes) { - if let sessionUUID, - let sessionType = billingHandler.sessionType(uuid: sessionUUID) - { - switch sessionType { - case .freeDrive: - billingHandler.stopBillingSession(with: sessionUUID) - beginNewSession(of: .activeGuidance) - case .activeGuidance: - if billingHandler.shouldStartNewBillingSession( - for: navigationRoutes.mainRoute.route, - remainingWaypoints: currentRouteProgress?.routeProgress.remainingWaypoints ?? [] - ) { - billingHandler.stopBillingSession(with: sessionUUID) - beginNewSession(of: .activeGuidance) - } - } - } else { - beginNewSession(of: .activeGuidance) - } - } - - @MainActor - private func verifyFreeDriveBillingSession() -> UUID? { - if let sessionUUID, - let sessionType = billingHandler.sessionType(uuid: sessionUUID) - { - switch sessionType { - case .freeDrive: - billingHandler.resumeBillingSession(with: sessionUUID) - case .activeGuidance: - billingHandler.stopBillingSession(with: sessionUUID) - beginNewSession(of: .freeDrive) - return sessionUUID - } - } else { - beginNewSession(of: .freeDrive) - } - return nil - } - - // MARK: - Implementation - - private let taskManager = TaskManager() - - @MainActor - private let billingHandler: BillingHandler - - private var sessionUUID: UUID? - - private var navigator: CoreNavigator { - configuration.navigator - } - - private let configuration: Configuration - - private var rerouteController: RerouteController? - - private var privateRouteProgress: RouteProgress? - - private let locationClient: LocationClient - - @MainActor - init(configuration: Configuration) { - self.configuration = configuration - self.locationClient = configuration.locationClient - self.roadMatching = .init( - roadGraph: configuration.navigator.roadGraph, - roadObjectStore: configuration.navigator.roadObjectStore, - roadObjectMatcher: configuration.navigator.roadObjectMatcher - ) - - self._session = .init(.init(state: .idle)) - self._mapMatching = .init(nil) - self._offlineFallbacks = .init() - self._voiceInstructions = .init() - self._bannerInstructions = .init() - self._waypointsArrival = .init() - self._rerouting = .init() - self._continuousAlternatives = .init() - self._fasterRoutes = .init() - self._routeRefreshing = .init() - self._eHorizonEvents = .init() - self._errors = .init() - self._routeProgress = .init(nil) - self._navigationRoutes = .init(nil) - self.rerouteController = configuration.navigator.rerouteController - self.billingHandler = configuration.billingHandler - let statusUpdateEvents = AsyncStreamBridge(bufferingPolicy: .bufferingNewest(1)) - self.statusUpdateEvents = statusUpdateEvents - - Task.detached { [weak self] in - for await status in statusUpdateEvents { - guard let self else { return } - - taskManager.cancellableTask { - await self.update(to: status) - } - } - } - - subscribeNotifications() - subscribeLocationUpdates() - - navigator.pause() - } - - deinit { - unsubscribeNotifications() - } - - // MARK: - NavigationStatus processing - - private func updateRouteProgress(with routes: NavigationRoutes?) async { - if let routes { - let waypoints = routes.mainRoute.route.legs.enumerated() - .reduce(into: [MapboxDirections.Waypoint]()) { partialResult, element in - if element.offset == 0 { - element.element.source.map { partialResult.append($0) } - } - element.element.destination.map { partialResult.append($0) } - } - let routeProgress = RouteProgress( - navigationRoutes: routes, - waypoints: waypoints, - congestionConfiguration: configuration.congestionConfig - ) - privateRouteProgress = routeProgress - await send(RouteProgressState(routeProgress: routeProgress)) - } else { - privateRouteProgress = nil - await send(RouteProgressState?.none) - } - } - - private func update(to status: NavigationStatus) async { - guard await currentSession.state != .idle else { - await send(NavigatorErrors.UnexpectedNavigationStatus()) - Log.warning( - "Received `NavigationStatus` while not in Active Guidance or Free Drive.", - category: .navigation - ) - return - } - - guard await billingSessionIsActive() else { - Log.error( - "Received `NavigationStatus` while billing session is not running.", - category: .billing - ) - return - } - - guard !Task.isCancelled else { return } - await updateMapMatching(status: status) - - guard case .activeGuidance = await currentSession.state else { - return - } - - guard !Task.isCancelled else { return } - await send(Session(state: .activeGuidance(.init(status.routeState)))) - - guard !Task.isCancelled else { return } - await updateIndices(status: status) - await updateAlternativesPassingForkPoint(status: status) - - if let privateRouteProgress, !Task.isCancelled { - await send(RouteProgressState(routeProgress: privateRouteProgress)) - } - await handleRouteProgressUpdates(status: status) - } - - func updateMapMatching(status: NavigationStatus) async { - let snappedLocation = CLLocation(status.location) - let roadName = status.localizedRoadName() - - let localeUnit: UnitSpeed? = { - switch status.speedLimit.localeUnit { - case .kilometresPerHour: - return .kilometersPerHour - case .milesPerHour: - return .milesPerHour - @unknown default: - Log.fault("Unhandled speed limit locale unit: \(status.speedLimit.localeUnit)", category: .navigation) - return nil - } - }() - - let signStandard: SignStandard = { - switch status.speedLimit.localeSign { - case .mutcd: - return .mutcd - case .vienna: - return .viennaConvention - @unknown default: - Log.fault( - "Unknown native speed limit sign locale \(status.speedLimit.localeSign)", - category: .navigation - ) - return .viennaConvention - } - }() - - let speedLimit: Measurement? = { - if let speed = status.speedLimit.speed?.doubleValue, let localeUnit { - return Measurement(value: speed, unit: localeUnit) - } else { - return nil - } - }() - - let currentSpeedUnit: UnitSpeed = { - if let localeUnit { - return localeUnit - } else { - switch signStandard { - case .mutcd: - return .milesPerHour - case .viennaConvention: - return .kilometersPerHour - } - } - }() - - await send(MapMatchingState( - location: navigator.rawLocation ?? snappedLocation, - mapMatchingResult: MapMatchingResult(status: status), - speedLimit: SpeedLimit( - value: speedLimit, - signStandard: signStandard - ), - currentSpeed: Measurement( - value: CLLocation(status.location).speed, - unit: .metersPerSecond - ).converted(to: currentSpeedUnit), - roadName: roadName.text.isEmpty ? nil : roadName - )) - } - - private var previousArrivalWaypoint: MapboxDirections.Waypoint? - - func handleRouteProgressUpdates(status: NavigationStatus) async { - guard let privateRouteProgress else { return } - - if let newSpokenInstruction = privateRouteProgress.currentLegProgress.currentStepProgress - .currentSpokenInstruction - { - await send(SpokenInstructionState(spokenInstruction: newSpokenInstruction)) - } - - if let newVisualInstruction = privateRouteProgress.currentLegProgress.currentStepProgress - .currentVisualInstruction - { - await send(VisualInstructionState(visualInstruction: newVisualInstruction)) - } - - let legProgress = privateRouteProgress.currentLegProgress - - // We are at least at the "You will arrive" instruction - if legProgress.remainingSteps.count <= 2 { - if status.routeState == .complete { - guard previousArrivalWaypoint != legProgress.leg.destination else { - return - } - if let destination = legProgress.leg.destination { - previousArrivalWaypoint = destination - let event: any WaypointArrivalEvent = if privateRouteProgress.isFinalLeg { - WaypointArrivalStatus.Events.ToFinalDestination(destination: destination) - } else { - WaypointArrivalStatus.Events.ToWaypoint( - waypoint: destination, - legIndex: privateRouteProgress.legIndex - ) - } - await send(WaypointArrivalStatus(event: event)) - } - let advancesToNextLeg = switch configuration.multilegAdvancing { - case .automatically: - true - case .manually(let approval): - await approval(.init(arrivedLegIndex: privateRouteProgress.legIndex)) - } - guard !privateRouteProgress.isFinalLeg, advancesToNextLeg else { - return - } - switchLeg(newLegIndex: Int(status.legIndex) + 1) - } - } - } - - fileprivate func updateAlternativesPassingForkPoint(status: NavigationStatus) async { - guard var navigationRoutes = currentNavigationRoutes else { return } - - guard navigationRoutes.updateForkPointPassed(with: status) else { return } - - privateRouteProgress?.updateAlternativeRoutes(using: navigationRoutes) - await send(navigationRoutes) - let alternativesStatus = AlternativesStatus( - event: AlternativesStatus.Events.Updated( - actualAlternativeRoutes: navigationRoutes.alternativeRoutes - ) - ) - await send(alternativesStatus) - } - - func updateIndices(status: NavigationStatus) async { - if let currentNavigationRoutes { - privateRouteProgress?.updateAlternativeRoutes(using: currentNavigationRoutes) - } - privateRouteProgress?.update(using: status) - } - - // MARK: - Notifications handling - - var subscriptions = Set() - - @MainActor - private func subscribeNotifications() { - rerouteController?.delegate = self - - [ - // Navigator - (Notification.Name.navigationDidSwitchToFallbackVersion, MapboxNavigator.fallbackToOffline(_:)), - (Notification.Name.navigationDidSwitchToTargetVersion, MapboxNavigator.restoreToOnline(_:)), - (Notification.Name.navigationStatusDidChange, MapboxNavigator.navigationStatusDidChange(_:)), - ( - Notification.Name.navigatorDidChangeAlternativeRoutes, - MapboxNavigator.navigatorDidChangeAlternativeRoutes(_:) - ), - ( - Notification.Name.navigatorDidFailToChangeAlternativeRoutes, - MapboxNavigator.navigatorDidFailToChangeAlternativeRoutes(_:) - ), - ( - Notification.Name.navigatorWantsSwitchToCoincideOnlineRoute, - MapboxNavigator.navigatorWantsSwitchToCoincideOnlineRoute(_:) - ), - (Notification.Name.routeRefreshDidUpdateAnnotations, MapboxNavigator.didRefreshAnnotations(_:)), - (Notification.Name.routeRefreshDidFailRefresh, MapboxNavigator.didFailToRefreshAnnotations(_:)), - // EH - ( - Notification.Name.electronicHorizonDidUpdatePosition, - MapboxNavigator.didUpdateElectronicHorizonPosition(_:) - ), - ( - Notification.Name.electronicHorizonDidEnterRoadObject, - MapboxNavigator.didEnterElectronicHorizonRoadObject(_:) - ), - ( - Notification.Name.electronicHorizonDidExitRoadObject, - MapboxNavigator.didExitElectronicHorizonRoadObject(_:) - ), - ( - Notification.Name.electronicHorizonDidPassRoadObject, - MapboxNavigator.didPassElectronicHorizonRoadObject(_:) - ), - ] - .forEach(subscribe(to:)) - - subscribeFasterRouteController() - } - - func disableTrackingBackgroundLocationIfNeeded() { - Task { - guard configuration.disableBackgroundTrackingLocation, - await currentSession.state == .freeDrive(.active) - else { - return - } - - await pauseFreeDrive() - await send(Session(state: .freeDrive(.active))) - } - } - - func restoreTrackingLocationIfNeeded() { - Task { - guard configuration.disableBackgroundTrackingLocation, - await currentSession.state == .freeDrive(.active) - else { - return - } - - await startFreeDrive() - } - } - - private func subscribeLocationUpdates() { - locationClient.locations - .receive(on: DispatchQueue.main) - .sink { [weak self] location in - guard let self else { return } - Task { @MainActor in - guard self.billingSessionIsActive() else { - Log.warning( - "Received location update while billing session is not running.", - category: .billing - ) - return - } - - self.navigator.updateLocation(location, completion: { _ in }) - } - }.store(in: &subscriptions) - } - - @MainActor - private func subscribeFasterRouteController() { - guard let fasterRouteController = configuration.fasterRouteController else { return } - - routeProgress - .compactMap { $0 } - .sink { currentRouteProgress in - fasterRouteController.checkForFasterRoute( - from: currentRouteProgress.routeProgress - ) - } - .store(in: &subscriptions) - - navigationRoutes - .sink { navigationRoutes in - fasterRouteController.navigationRoute = navigationRoutes?.mainRoute - } - .store(in: &subscriptions) - - mapMatching - .compactMap { $0 } - .sink { mapMatch in - fasterRouteController.currentLocation = mapMatch.enhancedLocation - } - .store(in: &subscriptions) - - rerouting - .sink { - fasterRouteController.isRerouting = $0.event is ReroutingStatus.Events.FetchingRoute - } - .store(in: &subscriptions) - - fasterRouteController.fasterRoutes - .receive(on: DispatchQueue.main) - .sink { [weak self] fasterRoutes in - Task { [weak self] in - self?.send( - FasterRoutesStatus( - event: FasterRoutesStatus.Events.Detected() - ) - ) - self?.taskManager.cancellableTask { [weak self] in - guard !Task.isCancelled else { return } - await self?.setRoutes( - navigationRoutes: fasterRoutes, - startLegIndex: 0, - reason: .fasterRoute - ) - } - } - } - .store(in: &subscriptions) - } - - private func subscribe( - to item: (name: Notification.Name, sink: (MapboxNavigator) -> (Notification) -> Void) - ) { - NotificationCenter.default - .publisher(for: item.name) - .sink { [weak self] notification in - self.map { item.sink($0)(notification) } - } - .store(in: &subscriptions) - } - - private func unsubscribeNotifications() { - rerouteController?.delegate = nil - subscriptions.removeAll() - } - - func fallbackToOffline(_ notification: Notification) { - Task { @MainActor in - rerouteController = configuration.navigator.rerouteController - rerouteController?.delegate = self - - guard let navigationRoutes = self.currentNavigationRoutes, - let privateRouteProgress else { return } - taskManager.cancellableTask { [self] in - guard !Task.isCancelled else { return } - await setRoutes( - navigationRoutes: navigationRoutes, - startLegIndex: privateRouteProgress.legIndex, - reason: .fallbackToOffline - ) - } - } - } - - func restoreToOnline(_ notification: Notification) { - Task { @MainActor in - rerouteController = configuration.navigator.rerouteController - rerouteController?.delegate = self - - guard let navigationRoutes = self.currentNavigationRoutes, - let privateRouteProgress else { return } - taskManager.cancellableTask { [self] in - guard !Task.isCancelled else { return } - await setRoutes( - navigationRoutes: navigationRoutes, - startLegIndex: privateRouteProgress.legIndex, - reason: .restoreToOnline - ) - } - } - } - - private func navigationStatusDidChange(_ notification: Notification) { - guard let userInfo = notification.userInfo, - let status = userInfo[NativeNavigator.NotificationUserInfoKey.statusKey] as? NavigationStatus - else { return } - statusUpdateEvents.yield(status) - } - - private func navigatorDidChangeAlternativeRoutes(_ notification: Notification) { - guard let alternativesAcceptionPolicy = configuration.alternativesAcceptionPolicy, - let mainRoute = currentNavigationRoutes?.mainRoute, - let userInfo = notification.userInfo, - let alternatives = - userInfo[NativeNavigator.NotificationUserInfoKey.alternativesListKey] as? [RouteAlternative] - else { - return - } - - Task { @MainActor in - navigator.setAlternativeRoutes(with: alternatives.map(\.route)) - { [weak self] result /* Result<[RouteAlternative], Error> */ in - guard let self else { return } - - Task { - switch result { - case .success(let routeAlternatives): - let alternativeRoutes = await AlternativeRoute.fromNative( - alternativeRoutes: routeAlternatives, - relateveTo: mainRoute - ) - - guard var navigationRoutes = self.currentNavigationRoutes else { return } - navigationRoutes.allAlternativeRoutesWithIgnored = alternativeRoutes - .filter { alternativeRoute in - if alternativesAcceptionPolicy.contains(.unfiltered) { - return true - } else { - if alternativesAcceptionPolicy.contains(.fasterRoutes), - alternativeRoute.expectedTravelTimeDelta < 0 - { - return true - } - if alternativesAcceptionPolicy.contains(.shorterRoutes), - alternativeRoute.distanceDelta < 0 - { - return true - } - } - return false - } - if let status = self.navigator.mostRecentNavigationStatus { - navigationRoutes.updateForkPointPassed(with: status) - } - await self.send(navigationRoutes) - await self - .send( - AlternativesStatus( - event: AlternativesStatus.Events.Updated( - actualAlternativeRoutes: navigationRoutes.alternativeRoutes - ) - ) - ) - case .failure(let updateError): - Log.warning( - "Failed to update alternative routes, error: \(updateError)", - category: .navigation - ) - let error = NavigatorErrors.FailedToUpdateAlternativeRoutes( - localizedDescription: updateError.localizedDescription - ) - await self.send(error) - } - } - } - } - } - - private func navigatorDidFailToChangeAlternativeRoutes(_ notification: Notification) { - guard let userInfo = notification.userInfo, - let message = userInfo[NativeNavigator.NotificationUserInfoKey.messageKey] as? String - else { - return - } - Log.error("Failed to change alternative routes: \(message)", category: .navigation) - Task { @MainActor in - send(NavigatorErrors.FailedToUpdateAlternativeRoutes(localizedDescription: message)) - } - } - - private func navigatorWantsSwitchToCoincideOnlineRoute(_ notification: Notification) { - guard configuration.prefersOnlineRoute, - let userInfo = notification.userInfo, - let onlineRoute = - userInfo[NativeNavigator.NotificationUserInfoKey.coincideOnlineRouteKey] as? RouteInterface - else { - return - } - - Task { - guard let route = await NavigationRoute(nativeRoute: onlineRoute) else { - return - } - let navigationRoutes = await NavigationRoutes( - mainRoute: route, - alternativeRoutes: [] - ) - - taskManager.cancellableTask { [self] in - guard !Task.isCancelled else { return } - await setRoutes( - navigationRoutes: navigationRoutes, - startLegIndex: 0, - reason: .restoreToOnline - ) - } - } - } - - func didUpdateElectronicHorizonPosition(_ notification: Notification) { - guard let position = notification.userInfo?[RoadGraph.NotificationUserInfoKey.positionKey] as? RoadGraph - .Position, - let startingEdge = notification.userInfo?[RoadGraph.NotificationUserInfoKey.treeKey] as? RoadGraph.Edge, - let updatesMostProbablePath = notification - .userInfo?[RoadGraph.NotificationUserInfoKey.updatesMostProbablePathKey] as? Bool, - let distancesByRoadObject = notification - .userInfo?[RoadGraph.NotificationUserInfoKey.distancesByRoadObjectKey] as? [DistancedRoadObject] - else { - return - } - - let event = EHorizonStatus.Events.PositionUpdated( - position: position, - startingEdge: startingEdge, - updatesMostProbablePath: updatesMostProbablePath, - distances: distancesByRoadObject - ) - Task { @MainActor in - send(EHorizonStatus(event: event)) - } - } - - func didEnterElectronicHorizonRoadObject(_ notification: Notification) { - guard let objectId = notification - .userInfo?[RoadGraph.NotificationUserInfoKey.roadObjectIdentifierKey] as? RoadObject.Identifier, - let hasEnteredFromStart = notification - .userInfo?[RoadGraph.NotificationUserInfoKey.didTransitionAtEndpointKey] as? Bool - else { - return - } - let event = EHorizonStatus.Events.RoadObjectEntered( - roadObjectId: objectId, - enteredFromStart: hasEnteredFromStart - ) - - Task { @MainActor in - send(EHorizonStatus(event: event)) - } - } - - func didExitElectronicHorizonRoadObject(_ notification: Notification) { - guard let objectId = notification - .userInfo?[RoadGraph.NotificationUserInfoKey.roadObjectIdentifierKey] as? RoadObject.Identifier, - let hasExitedFromEnd = notification - .userInfo?[RoadGraph.NotificationUserInfoKey.didTransitionAtEndpointKey] as? Bool - else { - return - } - let event = EHorizonStatus.Events.RoadObjectExited( - roadObjectId: objectId, - exitedFromEnd: hasExitedFromEnd - ) - Task { @MainActor in - send(EHorizonStatus(event: event)) - } - } - - func didPassElectronicHorizonRoadObject(_ notification: Notification) { - guard let objectId = notification - .userInfo?[RoadGraph.NotificationUserInfoKey.roadObjectIdentifierKey] as? RoadObject.Identifier - else { - return - } - let event = EHorizonStatus.Events.RoadObjectPassed(roadObjectId: objectId) - Task { @MainActor in - send(EHorizonStatus(event: event)) - } - } - - func didRefreshAnnotations(_ notification: Notification) { - guard let refreshRouteResult = notification - .userInfo?[NativeNavigator.NotificationUserInfoKey.refreshedRoutesResultKey] as? RouteRefreshResult, - let legIndex = notification.userInfo?[NativeNavigator.NotificationUserInfoKey.legIndexKey] as? UInt32, - let currentNavigationRoutes - else { - return - } - - Task { - guard case .activeGuidance = await currentSession.state else { - return - } - - var newMainRoute = currentNavigationRoutes.mainRoute - let isMainRouteUpdate = refreshRouteResult.updatedRoute.getRouteId() == - currentNavigationRoutes.mainRoute.routeId.rawValue - if isMainRouteUpdate { - guard let updatedMainRoute = await NavigationRoute(nativeRoute: refreshRouteResult.updatedRoute) - else { return } - newMainRoute = updatedMainRoute - } - let event = RefreshingStatus.Events.Refreshing() - await send(RefreshingStatus(event: event)) - - var refreshedNavigationRoutes = await NavigationRoutes( - mainRoute: newMainRoute, - alternativeRoutes: await AlternativeRoute.fromNative( - alternativeRoutes: refreshRouteResult.alternativeRoutes, - relateveTo: newMainRoute - ) - ) - if let status = self.navigator.mostRecentNavigationStatus { - refreshedNavigationRoutes.updateForkPointPassed(with: status) - } - self.privateRouteProgress = privateRouteProgress?.refreshingRoute( - with: refreshedNavigationRoutes, - legIndex: Int(legIndex), - legShapeIndex: 0, // TODO: NN should provide this value in `MBNNRouteRefreshObserver` - congestionConfiguration: configuration.congestionConfig - ) - await self.send(refreshedNavigationRoutes) - - if let privateRouteProgress { - await send(RouteProgressState(routeProgress: privateRouteProgress)) - } - let endEvent = RefreshingStatus.Events.Refreshed() - await send(RefreshingStatus(event: endEvent)) - } - } - - func didFailToRefreshAnnotations(_ notification: Notification) { - guard let refreshRouteFailure = notification - .userInfo?[NativeNavigator.NotificationUserInfoKey.refreshRequestErrorKey] as? RouteRefreshError, - refreshRouteFailure.refreshTtl == 0, - let currentNavigationRoutes - else { - return - } - - Task { - await send( - RefreshingStatus( - event: RefreshingStatus.Events.Invalidated( - navigationRoutes: currentNavigationRoutes - ) - ) - ) - } - } -} - -// MARK: - ReroutingControllerDelegate - -extension MapboxNavigator: ReroutingControllerDelegate { - func rerouteControllerWantsSwitchToAlternative( - _ rerouteController: RerouteController, - route: RouteInterface, - legIndex: Int - ) { - Task { - guard let navigationRoute = await NavigationRoute(nativeRoute: route) else { - return - } - - taskManager.cancellableTask { [self] in - guard !Task.isCancelled else { return } - await setRoutes( - navigationRoutes: NavigationRoutes( - mainRoute: navigationRoute, - alternativeRoutes: [] - ), - startLegIndex: legIndex, - reason: .alternatives - ) - } - } - } - - func rerouteControllerDidDetectReroute(_ rerouteController: RerouteController) { - Log.debug("Reroute was detected.", category: .navigation) - Task { @MainActor in - send( - ReroutingStatus( - event: ReroutingStatus.Events.FetchingRoute() - ) - ) - } - } - - func rerouteControllerDidRecieveReroute(_ rerouteController: RerouteController, routesData: RoutesData) { - Log.debug( - "Reroute was fetched with primary route id '\(routesData.primaryRoute().getRouteId())' and \(routesData.alternativeRoutes().count) alternative route(s).", - category: .navigation - ) - Task { - guard let navigationRoutes = try? await NavigationRoutes(routesData: routesData) else { - Log.error( - "Reroute was fetched but could not convert it to `NavigationRoutes`.", - category: .navigation - ) - return - } - taskManager.cancellableTask { [self] in - guard !Task.isCancelled else { return } - await setRoutes( - navigationRoutes: navigationRoutes, - startLegIndex: 0, - reason: .reroute - ) - } - } - } - - func rerouteControllerDidCancelReroute(_ rerouteController: RerouteController) { - Log.warning("Reroute was cancelled.", category: .navigation) - Task { @MainActor in - send( - ReroutingStatus( - event: ReroutingStatus.Events.Interrupted() - ) - ) - send(NavigatorErrors.InterruptedReroute(underlyingError: nil)) - } - } - - func rerouteControllerDidFailToReroute(_ rerouteController: RerouteController, with error: DirectionsError) { - Log.error("Failed to reroute, error: \(error)", category: .navigation) - Task { @MainActor in - send( - ReroutingStatus( - event: ReroutingStatus.Events.Failed(error: error) - ) - ) - send(NavigatorErrors.InterruptedReroute(underlyingError: error)) - } - } -} - -extension MapboxNavigator { - @MainActor - private func send(_ details: NavigationRoutes?) { - if details == nil { - previousArrivalWaypoint = nil - } - _navigationRoutes.emit(details) - } - - @MainActor - private func send(_ details: Session) { - _session.emit(details) - } - - @MainActor - private func send(_ details: MapMatchingState) { - _mapMatching.emit(details) - } - - @MainActor - private func send(_ details: RouteProgressState?) { - _routeProgress.emit(details) - } - - @MainActor - private func send(_ details: FallbackToTilesState) { - _offlineFallbacks.emit(details) - } - - @MainActor - private func send(_ details: SpokenInstructionState) { - _voiceInstructions.emit(details) - } - - @MainActor - private func send(_ details: VisualInstructionState) { - _bannerInstructions.emit(details) - } - - @MainActor - private func send(_ details: WaypointArrivalStatus) { - _waypointsArrival.emit(details) - } - - @MainActor - private func send(_ details: ReroutingStatus) { - _rerouting.emit(details) - } - - @MainActor - private func send(_ details: AlternativesStatus) { - _continuousAlternatives.emit(details) - } - - @MainActor - private func send(_ details: FasterRoutesStatus) { - _fasterRoutes.emit(details) - } - - @MainActor - private func send(_ details: RefreshingStatus) { - _routeRefreshing.emit(details) - } - - @MainActor - private func send(_ details: NavigatorError) { - _errors.emit(details) - } - - @MainActor - private func send(_ details: EHorizonStatus) { - _eHorizonEvents.emit(details) - } -} - -// MARK: - TaskManager - -extension MapboxNavigator { - fileprivate final class TaskManager: Sendable { - private let tasksInFlight_ = UnfairLocked([String: Task]()) - func cancellableTask( - id: String = #function, - operation: @Sendable @escaping () async throws -> Void - ) rethrows { - Task { - defer { - _ = tasksInFlight_.mutate { - $0.removeValue(forKey: id) - } - } - - guard !barrier.read() else { return } - let task = Task { try await operation() } - tasksInFlight_.mutate { - $0[id]?.cancel() - $0[id] = task - } - _ = try await task.value - } - } - - func cancelTasks() { - tasksInFlight_.mutate { - $0.forEach { - $0.value.cancel() - } - $0.removeAll() - } - } - - private let barrier: UnfairLocked = .init(false) - - @MainActor - func withBarrier(_ operation: () -> Void) { - barrier.update(true) - cancelTasks() - operation() - barrier.update(false) - } - } -} - -extension MapboxNavigator.SetRouteReason { - var navNativeValue: MapboxNavigationNative.SetRoutesReason { - switch self { - case .newRoute: - return .newRoute - case .alternatives: - return .alternative - case .reroute: - return .reroute - case .fallbackToOffline: - return .fallbackToOffline - case .restoreToOnline: - return .restoreToOnline - case .fasterRoute: - return .fastestRoute - } - } -} - -extension NavigationRoutes { - @discardableResult - mutating func updateForkPointPassed(with status: NavigationStatus) -> Bool { - let newPassedForkPointRouteIds = Set( - status.alternativeRouteIndices - .compactMap { $0.isForkPointPassed ? $0.routeId : nil } - ) - let oldPassedForkPointRouteIds = Set( - allAlternativeRoutesWithIgnored - .compactMap { $0.isForkPointPassed ? $0.routeId.rawValue : nil } - ) - guard newPassedForkPointRouteIds != oldPassedForkPointRouteIds else { return false } - - for (index, route) in allAlternativeRoutesWithIgnored.enumerated() { - allAlternativeRoutesWithIgnored[index].isForkPointPassed = - newPassedForkPointRouteIds.contains(route.routeId.rawValue) - } - return true - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationLocationManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationLocationManager.swift deleted file mode 100644 index c7d56f650..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationLocationManager.swift +++ /dev/null @@ -1,44 +0,0 @@ -import CoreLocation -import Foundation -#if os(iOS) -import UIKit -#endif - -/// ``NavigationLocationManager`` is the base location manager which handles permissions and background modes. -open class NavigationLocationManager: CLLocationManager { - @MainActor - override public init() { - super.init() - - requestWhenInUseAuthorization() - - if Bundle.main.backgroundModes.contains("location") { - allowsBackgroundLocationUpdates = true - } - - delegate = self - } - - /// Indicates whether the location manager is providing simulated locations. - open var simulatesLocation: Bool = false - - public weak var locationDelegate: NavigationLocationManagerDelegate? = nil -} - -public protocol NavigationLocationManagerDelegate: AnyObject { - func navigationLocationManager( - _ locationManager: NavigationLocationManager, - didReceiveNewLocation location: CLLocation - ) -} - -extension NavigationLocationManager: CLLocationManagerDelegate { - open func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - if let location = locations.last { - locationDelegate?.navigationLocationManager( - self, - didReceiveNewLocation: location - ) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationRoutes.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationRoutes.swift deleted file mode 100644 index d922d0b58..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/NavigationRoutes.swift +++ /dev/null @@ -1,420 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import MapboxDirections -@preconcurrency import MapboxNavigationNative -import Turf - -/// Contains a selection of ``NavigationRoute`` and it's related ``AlternativeRoute``'s, which can be sued for -/// navigation. -public struct NavigationRoutes: Equatable, @unchecked Sendable { - /// A route choosed to navigate on. - public internal(set) var mainRoute: NavigationRoute - /// Suggested alternative routes. - /// - /// To select one of the alterntives as a main route, see ``selectingAlternativeRoute(at:)`` and - /// ``selecting(alternativeRoute:)`` methods. - public var alternativeRoutes: [AlternativeRoute] { - allAlternativeRoutesWithIgnored.filter { !$0.isForkPointPassed } - } - - /// A list of ``Waypoint``s visited along the routes. - public internal(set) var waypoints: [Waypoint] - /// A deadline after which the routes from this `RouteResponse` are eligable for refreshing. - /// - /// `nil` value indicates that route refreshing is not available for related routes. - public internal(set) var refreshInvalidationDate: Date? - /// Contains a map of `JSONObject`'s which were appended in the original route response, but are not recognized by - /// the SDK. - public internal(set) var foreignMembers: JSONObject = [:] - - var allAlternativeRoutesWithIgnored: [AlternativeRoute] - - var isCustomExternalRoute: Bool { - mainRoute.nativeRoute.getRouterOrigin() == .customExternal - } - - init(routesData: RoutesData) async throws { - let routeResponse = try await routesData.primaryRoute().convertToDirectionsRouteResponse() - try self.init(routesData: routesData, routeResponse: routeResponse) - } - - private init(routesData: RoutesData, routeResponse: RouteResponse) throws { - guard let routes = routeResponse.routes else { - Log.error("Unable to get routes", category: .navigation) - throw NavigationRoutesError.emptyRoutes - } - - guard routes.count == routesData.alternativeRoutes().count + 1 else { - Log.error("Routes mismatched", category: .navigation) - throw NavigationRoutesError.incorrectRoutesNumber - } - - let mainRoute = routes[Int(routesData.primaryRoute().getRouteIndex())] - - var alternativeRoutes = [AlternativeRoute]() - for routeAlternative in routesData.alternativeRoutes() { - guard let alternativeRoute = AlternativeRoute( - mainRoute: mainRoute, - alternativeRoute: routes[Int(routeAlternative.route.getRouteIndex())], - nativeRouteAlternative: routeAlternative - ) else { - Log.error("Unable to convert alternative route with id: \(routeAlternative.id)", category: .navigation) - continue - } - - alternativeRoutes.append(alternativeRoute) - } - - self.mainRoute = NavigationRoute(route: mainRoute, nativeRoute: routesData.primaryRoute()) - self.allAlternativeRoutesWithIgnored = alternativeRoutes - self.waypoints = routeResponse.waypoints ?? [] - self.refreshInvalidationDate = routeResponse.refreshInvalidationDate - self.foreignMembers = routeResponse.foreignMembers - } - - init(mainRoute: NavigationRoute, alternativeRoutes: [AlternativeRoute]) async { - self.mainRoute = mainRoute - self.allAlternativeRoutesWithIgnored = alternativeRoutes - - let response = try? await mainRoute.nativeRoute.convertToDirectionsRouteResponse() - self.waypoints = response?.waypoints ?? [] - self.refreshInvalidationDate = response?.refreshInvalidationDate - if let foreignMembers = response?.foreignMembers { - self.foreignMembers = foreignMembers - } - } - - @_spi(MapboxInternal) - public init(routeResponse: RouteResponse, routeIndex: Int, responseOrigin: RouterOrigin) async throws { - let options = NavigationRoutes.validatedRouteOptions(options: routeResponse.options) - - let encoder = JSONEncoder() - encoder.userInfo[.options] = options - let routeData = try encoder.encode(routeResponse) - - let routeRequest = Directions.url(forCalculating: options, credentials: routeResponse.credentials) - .absoluteString - - let parsedRoutes = RouteParser.parseDirectionsResponse( - forResponseDataRef: .init(data: routeData), - request: routeRequest, - routeOrigin: responseOrigin - ) - if parsedRoutes.isValue(), - var routes = parsedRoutes.value as? [RouteInterface], - routes.indices.contains(routeIndex) - { - let routesData = RouteParser.createRoutesData( - forPrimaryRoute: routes.remove(at: routeIndex), - alternativeRoutes: routes - ) - let navigationRoutes = try NavigationRoutes(routesData: routesData, routeResponse: routeResponse) - self = navigationRoutes - self.waypoints = routeResponse.waypoints ?? [] - self.refreshInvalidationDate = routeResponse.refreshInvalidationDate - self.foreignMembers = routeResponse.foreignMembers - } else if parsedRoutes.isError(), - let error = parsedRoutes.error - { - Log.error("Failed to parse routes with error: \(error)", category: .navigation) - throw NavigationRoutesError.responseParsingError(description: error as String) - } else { - Log.error("Unexpected error during routes parsing.", category: .navigation) - throw NavigationRoutesError.unknownError - } - } - - func asRoutesData() -> RoutesData { - return RouteParser.createRoutesData( - forPrimaryRoute: mainRoute.nativeRoute, - alternativeRoutes: alternativeRoutes.map(\.nativeRoute) - ) - } - - func selectingMostSimilar(to route: Route) async -> NavigationRoutes { - let target = route.description - - var candidates = [mainRoute.route] - candidates.append(contentsOf: alternativeRoutes.map(\.route)) - - guard let bestCandidate = candidates.map({ - (route: $0, editDistance: $0.description.minimumEditDistance(to: target)) - }).enumerated().min(by: { $0.element.editDistance < $1.element.editDistance }) else { return self } - - // If the most similar route is still more than 50% different from the original route, - // we fallback to the fastest route which index is 0. - let totalLength = Double(bestCandidate.element.route.description.count + target.description.count) - guard totalLength > 0 else { return self } - let differenceScore = Double(bestCandidate.element.editDistance) / totalLength - // Comparing to 0.25 as for "replacing the half of the string", since we add target and candidate lengths - // together - // Algorithm proposal: https://github.com/mapbox/mapbox-navigation-ios/pull/3664#discussion_r772194977 - guard differenceScore < 0.25 else { return self } - - if bestCandidate.offset > 0 { - return await selectingAlternativeRoute(at: bestCandidate.offset - 1) ?? self - } else { - return self - } - } - - /// Returns a new ``NavigationRoutes`` instance, wich has corresponding ``AlternativeRoute`` set as the main one. - /// - /// This operation requires re-parsing entire routes data, because all alternative's relative stats will not remain - /// the same after changing the ``mainRoute``. - /// - /// - parameter index: Index in ``alternativeRoutes`` array to assign as a main route. - /// - returns: New ``NavigationRoutes`` instance, with new `alternativeRoute` set as the main one, or `nil` if the - /// `index` is out of bounds.. - public func selectingAlternativeRoute(at index: Int) async -> NavigationRoutes? { - guard self.alternativeRoutes.indices.contains(index) else { - return nil - } - var alternativeRoutes = alternativeRoutes - - let alternativeRoute = alternativeRoutes.remove(at: index) - - let routesData = RouteParser.createRoutesData( - forPrimaryRoute: alternativeRoute.nativeRoute, - alternativeRoutes: alternativeRoutes.map(\.nativeRoute) + [mainRoute.nativeRoute] - ) - - let newMainRoute = NavigationRoute(route: alternativeRoute.route, nativeRoute: alternativeRoute.nativeRoute) - - var newAlternativeRoutes = alternativeRoutes.compactMap { oldAlternative -> AlternativeRoute? in - guard let nativeRouteAlternative = routesData.alternativeRoutes() - .first(where: { $0.route.getRouteId() == oldAlternative.routeId.rawValue }) - else { - Log.warning( - "Unable to create an alternative route for \(oldAlternative.routeId.rawValue)", - category: .navigation - ) - return nil - } - return AlternativeRoute( - mainRoute: newMainRoute.route, - alternativeRoute: oldAlternative.route, - nativeRouteAlternative: nativeRouteAlternative - ) - } - - if let nativeRouteAlternative = routesData.alternativeRoutes() - .first(where: { $0.route.getRouteId() == mainRoute.routeId.rawValue }), - let newAlternativeRoute = AlternativeRoute( - mainRoute: newMainRoute.route, - alternativeRoute: mainRoute.route, - nativeRouteAlternative: nativeRouteAlternative - ) - { - newAlternativeRoutes.append(newAlternativeRoute) - } else { - Log.warning( - "Unable to create an alternative route: \(mainRoute.routeId.rawValue) for a new main route: \(alternativeRoute.routeId.rawValue)", - category: .navigation - ) - } - - return await .init(mainRoute: newMainRoute, alternativeRoutes: newAlternativeRoutes) - } - - /// Returns a new ``NavigationRoutes`` instance, wich has corresponding ``AlternativeRoute`` set as the main one. - /// - /// This operation requires re-parsing entire routes data, because all alternative's relative stats will not remain - /// the same after changing the ``mainRoute``. - /// - /// - parameter alternativeRoute: An ``AlternativeRoute`` to assign as main. - /// - returns: New ``NavigationRoutes`` instance, with `alternativeRoute` set as the main one, or `nil` if current - /// instance does not contain this alternative. - public func selecting(alternativeRoute: AlternativeRoute) async -> NavigationRoutes? { - guard let index = alternativeRoutes.firstIndex(where: { $0 == alternativeRoute }) else { - return nil - } - return await selectingAlternativeRoute(at: index) - } - - static func validatedRouteOptions(options: ResponseOptions) -> RouteOptions { - switch options { - case .match(let matchOptions): - return RouteOptions(matchOptions: matchOptions) - case .route(let options): - return options - } - } - - /// A convenience method to get a list of all included `Route`s, optionally filtering it in the process. - /// - /// - parameter isIncluded: A callback, used to filter the routes. - /// - returns: A list of all included routes, filtered by `isIncluded` rule. - public func allRoutes(_ isIncluded: (Route) -> Bool = { _ in true }) -> [Route] { - var routes: [Route] = [] - if isIncluded(mainRoute.route) { - routes.append(mainRoute.route) - } - routes.append(contentsOf: alternativeRoutes.lazy.map(\.route).filter(isIncluded)) - return routes - } - - /// Convenience method to comare routes set with another ``NavigationRoutes`` instance. - /// - /// - note: The comparison is done by ``NavigationRoute/routeId``. - /// - /// - parameter otherRoutes: A ``NavigationRoutes`` instance against which to compare. - /// - returns: `true` if `otherRoutes` contains exactly the same collection of routes, `false` - otherwise. - public func containsSameRoutes(as otherRoutes: NavigationRoutes) -> Bool { - let currentRouteIds = Set(alternativeRoutes.map(\.routeId) + [mainRoute.routeId]) - let newRouteIds = Set(otherRoutes.alternativeRoutes.map(\.routeId) + [otherRoutes.mainRoute.routeId]) - return currentRouteIds == newRouteIds - } -} - -/// Wraps a route object used across the Navigation SDK. -public struct NavigationRoute: Sendable { - /// // A `Route` object that the current navigation route represents. - public let route: Route - /// Unique route id. - public let routeId: RouteId - - public let nativeRoute: RouteInterface - - public init?(nativeRoute: RouteInterface) async { - self.nativeRoute = nativeRoute - self.routeId = .init(rawValue: nativeRoute.getRouteId()) - - guard let route = try? await nativeRoute.convertToDirectionsRoute() else { - return nil - } - - self.route = route - } - - init(route: Route, nativeRoute: RouteInterface) { - self.nativeRoute = nativeRoute - self.route = route - self.routeId = .init(rawValue: nativeRoute.getRouteId()) - } - - private let _routeOptions: NSLocked<(initialized: Bool, options: RouteOptions?)> = .init((false, nil)) - public var routeOptions: RouteOptions? { - _routeOptions.mutate { state in - if state.initialized { - return state.options - } else { - state.initialized = true - if let newOptions = getRouteOptions() { - state.options = newOptions - return newOptions - } else { - return nil - } - } - } - } - - private func getRouteOptions() -> RouteOptions? { - guard let url = URL(string: nativeRoute.getRequestUri()) else { - return nil - } - - return RouteOptions(url: url) - } -} - -extension NavigationRoute: Equatable { - public static func == (lhs: NavigationRoute, rhs: NavigationRoute) -> Bool { - return lhs.routeId == rhs.routeId && - lhs.route == rhs.route - } -} - -extension RouteInterface { - func convertToDirectionsRouteResponse() async throws -> RouteResponse { - guard let requestURL = URL(string: getRequestUri()), - let routeOptions = RouteOptions(url: requestURL) - else { - Log.error( - "Couldn't extract response and request data to parse `RouteInterface` into `RouteResponse`", - category: .navigation - ) - throw NavigationRoutesError.noRequestData - } - - let credentials = Credentials(requestURL: requestURL) - let decoder = JSONDecoder() - decoder.userInfo[.options] = routeOptions - decoder.userInfo[.credentials] = credentials - - do { - let ref = getResponseJsonRef() - return try decoder.decode(RouteResponse.self, from: ref.data) - } catch { - Log.error( - "Couldn't parse `RouteInterface` into `RouteResponse` with error: \(error)", - category: .navigation - ) - throw NavigationRoutesError.encodingError(underlyingError: error) - } - } - - func convertToDirectionsRoute() async throws -> Route { - do { - guard let routes = try await convertToDirectionsRouteResponse().routes else { - Log.error("Converting to directions route yielded no routes.", category: .navigation) - throw NavigationRoutesError.emptyRoutes - } - guard routes.count > getRouteIndex() else { - Log.error( - "Converting to directions route yielded incorrect number of routes (expected at least \(getRouteIndex() + 1) but have \(routes.count).", - category: .navigation - ) - throw NavigationRoutesError.incorrectRoutesNumber - } - return routes[Int(getRouteIndex())] - } catch { - Log.error( - "Parsing `RouteInterface` into `Route` resulted in no routes", - category: .navigation - ) - throw error - } - } -} - -public struct RouteId: Hashable, Sendable, Codable, CustomStringConvertible { - var rawValue: String - - public init(rawValue: String) { - self.rawValue = rawValue - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - self.rawValue = try container.decode(String.self) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(rawValue) - } - - public var description: String { - "RouteId(\(rawValue)" - } -} - -/// The error describing a possible cause of failing to instantiate the ``NavigationRoutes`` object. -public enum NavigationRoutesError: Error { - /// Could not correctly encode provided data into a valid JSON. - /// - /// See the associated error for more details. - case encodingError(underlyingError: Error?) - /// Failed to compose routes object(s) from the JSON representation. - case responseParsingError(description: String) - /// Could not extract route request parameters from the JSON representation. - case noRequestData - /// Routes parsing resulted in an empty routes list. - case emptyRoutes - /// The number of decoded routes does not match the expected amount - case incorrectRoutesNumber - /// An unexpected error occurred during parsing. - case unknownError -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Navigator.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Navigator.swift deleted file mode 100644 index 1d3afd0b0..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Navigator.swift +++ /dev/null @@ -1,403 +0,0 @@ -import Combine -import CoreLocation -import MapboxDirections -import MapboxNavigationNative - -// MARK: - NavigationEvent - -/// The base for all ``MapboxNavigation`` events. -public protocol NavigationEvent: Equatable, Sendable {} -extension NavigationEvent { - fileprivate func compare(to other: any NavigationEvent) -> Bool { - guard let other = other as? Self else { - return false - } - return self == other - } -} - -// MARK: - SessionState - -/// Navigation session details. -public struct Session: Equatable, Sendable { - /// Current session state. - public let state: State - - /// Describes possible navigation states. - public enum State: Equatable, Sendable { - /// The navigator is idle and is not tracking user location. - case idle - /// The navigator observes user location and matches it to the road network. - case freeDrive(FreeDriveState) // MBNNRouteStateInvalid * - /// The navigator tracks user progress along the given route. - case activeGuidance(ActiveGuidanceState) - - /// Flags if navigator is currently active. - public var isTripSessionActive: Bool { - return self != .idle - } - - /// Describes possible Free Drive states - public enum FreeDriveState: Sendable { - /// Free drive is paused. - /// - /// The navigator does not currently tracks user location, but can be resumed any time. - /// Unlike switching to the ``Session/State-swift.enum/idle`` state, pausing the Free drive does not - /// interrupt the navigation session. - case paused - /// The navigator observes user location and matches it to the road network. - /// - /// Unlike switching to the ``Session/State-swift.enum/idle`` state, pausing the Free drive does not - /// interrupt the navigation session. - case active - } - - /// Describes possible Active Guidance states. - public enum ActiveGuidanceState: Sendable { - /// Initial state when starting a new route. - case initialized - /// The Navigation process is nominal. - /// - /// The navigator tracks user position and progress. - case tracking // MBNNRouteStateTracking - /// The navigator detected user went off the route. - case offRoute // MBNNRouteStateUncertain * - /// The navigator experiences troubles determining it's state. - /// - /// This may be signaled when navigator is judjing if user is still on the route or is wandering off, or - /// when GPS signal quality has dropped, or due to some other technical conditions. - /// Unless `offRoute` is reported - it is still treated as user progressing the route. - case uncertain // MBNNRouteStateInitialized + MBNNRouteStateUncertain + MBNNRouteStateInvalid(?) - /// The user has arrived to the final destination. - case complete // MBNNRouteStateComplete - - init(_ routeState: RouteState) { - switch routeState { - case .invalid, .uncertain: - self = .uncertain - case .initialized: - self = .initialized - case .tracking: - self = .tracking - case .complete: - self = .complete - case .offRoute: - self = .offRoute - @unknown default: - self = .uncertain - } - } - } - } -} - -// MARK: - RouteProgressState - -/// Route progress update event details. -public struct RouteProgressState: Sendable { - /// Actual ``RouteProgress``. - public let routeProgress: RouteProgress -} - -// MARK: - MapMatchingState - -/// Map matching update event details. -public struct MapMatchingState: Equatable, @unchecked Sendable { - /// Current user raw location. - public let location: CLLocation - /// Current user matched location. - public let mapMatchingResult: MapMatchingResult - /// Actual speed limit. - public let speedLimit: SpeedLimit - /// Detected actual user speed. - public let currentSpeed: Measurement - /// Current road name, if available. - public let roadName: RoadName? - - /// The best possible location update, snapped to the route or map matched to the road if possible - public var enhancedLocation: CLLocation { - mapMatchingResult.enhancedLocation - } -} - -// MARK: - FallbackToTilesState - -/// Tiles fallback update event details. -public struct FallbackToTilesState: Equatable, Sendable { - /// Flags if the Navigator is currently using latest known tiles version. - public let usingLatestTiles: Bool -} - -// MARK: - SpokenInstructionState - -/// Voice instructions update event details. -public struct SpokenInstructionState: Equatable, Sendable { - /// Actual ``SpokenInstruction`` to be pronounced. - public let spokenInstruction: SpokenInstruction -} - -// MARK: - VisualInstructionState - -/// Visual instructions update event details. -public struct VisualInstructionState: Equatable, Sendable { - /// Actual visual instruction to be displayed. - public let visualInstruction: VisualInstructionBanner -} - -// MARK: - WaypointArrivalStatus - -/// The base for all ``WaypointArrivalStatus`` events. -public protocol WaypointArrivalEvent: NavigationEvent {} - -/// Waypoint arrival update event details. -public struct WaypointArrivalStatus: Equatable, Sendable { - public static func == (lhs: WaypointArrivalStatus, rhs: WaypointArrivalStatus) -> Bool { - lhs.event.compare(to: rhs.event) - } - - /// Actual event details. - /// - /// See ``WaypointArrivalEvent`` implementations for possible event types. - public let event: any WaypointArrivalEvent - - public enum Events { - /// User has arrived to the final destination. - public struct ToFinalDestination: WaypointArrivalEvent, @unchecked Sendable { - /// Final destination waypoint. - public let destination: Waypoint - } - - /// User has arrived to the intermediate waypoint. - public struct ToWaypoint: WaypointArrivalEvent, @unchecked Sendable { - /// The waypoint user has arrived to. - public let waypoint: Waypoint - /// Waypoint's leg index. - public let legIndex: Int - } - - /// Next leg navigation has started. - public struct NextLegStarted: WaypointArrivalEvent, @unchecked Sendable { - /// New actual leg index in the route. - public let newLegIndex: Int - } - } -} - -// MARK: - ReroutingStatus - -/// The base for all ``ReroutingStatus`` events. -public protocol ReroutingEvent: NavigationEvent {} - -/// Rerouting update event details. -public struct ReroutingStatus: Equatable, Sendable { - public static func == (lhs: ReroutingStatus, rhs: ReroutingStatus) -> Bool { - lhs.event.compare(to: rhs.event) - } - - /// Actual event details. - /// - /// See ``ReroutingEvent`` implementations for possible event types. - public let event: any ReroutingEvent - - public enum Events { - /// Reroute event was triggered and SDK is currently fetching a new route. - public struct FetchingRoute: ReroutingEvent, Sendable {} - /// The reroute process was manually interrupted. - public struct Interrupted: ReroutingEvent, Sendable {} - /// The reroute process has failed with an error. - public struct Failed: ReroutingEvent, Sendable { - /// The underlying error. - public let error: DirectionsError - } - - /// The reroute process has successfully fetched a route and completed the process. - public struct Fetched: ReroutingEvent, Sendable {} - } -} - -// MARK: - AlternativesStatus - -/// The base for all ``AlternativesStatus`` events. -public protocol AlternativesEvent: NavigationEvent {} - -/// Continuous alternatives update event details. -public struct AlternativesStatus: Equatable, Sendable { - public static func == (lhs: AlternativesStatus, rhs: AlternativesStatus) -> Bool { - lhs.event.compare(to: rhs.event) - } - - /// Actual event details. - /// - /// See ``AlternativesEvent`` implementations for possible event types. - public let event: any AlternativesEvent - - public enum Events { - /// The list of actual continuous alternatives was updated. - public struct Updated: AlternativesEvent, Sendable { - /// Currently actual list of alternative routes. - public let actualAlternativeRoutes: [AlternativeRoute] - } - - /// The navigator switched to the alternative route. The previous main route is an alternative now. - public struct SwitchedToAlternative: AlternativesEvent, Sendable { - /// The current navigation routes after switching to the alternative route. - public let navigationRoutes: NavigationRoutes - } - } -} - -// MARK: - FasterRoutesStatus - -/// The base for all ``FasterRoutesStatus`` events. -public protocol FasterRoutesEvent: NavigationEvent {} - -/// Faster route update event details. -public struct FasterRoutesStatus: Equatable, Sendable { - public static func == (lhs: FasterRoutesStatus, rhs: FasterRoutesStatus) -> Bool { - lhs.event.compare(to: rhs.event) - } - - /// Actual event details. - /// - /// See ``FasterRoutesEvent`` implementations for possible event types. - public let event: any FasterRoutesEvent - - public enum Events { - /// The SDK has detected a faster route possibility. - public struct Detected: FasterRoutesEvent, Sendable {} - /// The SDK has applied the faster route. - public struct Applied: FasterRoutesEvent, Sendable {} - } -} - -// MARK: - RefreshingStatus - -/// The base for all ``RefreshingStatus`` events. -public protocol RefreshingEvent: NavigationEvent {} - -/// Route refreshing update event details. -public struct RefreshingStatus: Equatable, Sendable { - public static func == (lhs: RefreshingStatus, rhs: RefreshingStatus) -> Bool { - lhs.event.compare(to: rhs.event) - } - - /// Actual event details. - /// - /// See ``RefreshingEvent`` implementations for possible event types. - public let event: any RefreshingEvent - - public enum Events { - /// The route refreshing process has begun. - public struct Refreshing: RefreshingEvent, Sendable {} - /// The route has been refreshed. - public struct Refreshed: RefreshingEvent, Sendable {} - /// Indicates that current route's refreshing is no longer available. - /// - /// It is strongly recommended to request a new route. Refreshing TTL has expired and the route will no longer - /// recieve refreshing updates, which may lead to suboptimal navigation experience. - public struct Invalidated: RefreshingEvent, Sendable { - /// The routes for which refreshing is no longer available. - public let navigationRoutes: NavigationRoutes - } - } -} - -// MARK: - EHorizonStatus - -/// The base for all ``EHorizonStatus`` events. -public protocol EHorizonEvent: NavigationEvent {} - -/// Electronic horizon update event details. -public struct EHorizonStatus: Equatable, Sendable { - public static func == (lhs: EHorizonStatus, rhs: EHorizonStatus) -> Bool { - lhs.event.compare(to: rhs.event) - } - - /// Actual event details. - /// - /// See ``EHorizonEvent`` implementations for possible event types. - public let event: any EHorizonEvent - - public enum Events { - /// EH position withing the road graph has changed. - public struct PositionUpdated: Sendable, EHorizonEvent { - /// New EH position. - public let position: RoadGraph.Position - /// New starting edge of the graph - public let startingEdge: RoadGraph.Edge - /// Flags if MPP was updated. - public let updatesMostProbablePath: Bool - /// Distances for upcoming road objects. - public let distances: [DistancedRoadObject] - } - - /// EH position has entered a road object. - public struct RoadObjectEntered: Sendable, EHorizonEvent { - /// Related road object ID. - public let roadObjectId: RoadObject.Identifier - /// Flags if entrance was from object's beginning. - public let enteredFromStart: Bool - } - - /// EH position has left a road object - public struct RoadObjectExited: Sendable, EHorizonEvent { - /// Related road object ID. - public let roadObjectId: RoadObject.Identifier - /// Flags if object was left through it's ending. - public let exitedFromEnd: Bool - } - - /// EH position has passed point or gantry objects - public struct RoadObjectPassed: Sendable, EHorizonEvent { - /// Related road object ID. - public let roadObjectId: RoadObject.Identifier - } - } -} - -// MARK: - NavigatorError - -/// The base for all ``NavigatorErrors``. -public protocol NavigatorError: Error {} - -public enum NavigatorErrors { - /// The SDK has failed to set a route to the Navigator. - public struct FailedToSetRoute: NavigatorError { - /// Underlying error description. - public let underlyingError: Error? - } - - /// Switching to the alternative route has failed. - public struct FailedToSelectAlternativeRoute: NavigatorError {} - /// Updating the list of alternative routes has failed. - public struct FailedToUpdateAlternativeRoutes: NavigatorError { - /// Localized description. - public let localizedDescription: String - } - - /// Switching route legs has failed. - public struct FailedToSelectRouteLeg: NavigatorError {} - /// Failed to switch the navigator state to `idle`. - public struct FailedToSetToIdle: NavigatorError {} - /// Failed to pause the free drive session. - public struct FailedToPause: NavigatorError {} - /// Unexpectedly received NN status when in `idle` state. - public struct UnexpectedNavigationStatus: NavigatorError {} - /// Rerouting process was not completed successfully. - public struct InterruptedReroute: NavigatorError { - /// Underlying error description. - public let underlyingError: Error? - } -} - -// MARK: - RoadMatching - -/// Description of the road graph network and related road objects. -public struct RoadMatching: Sendable { - /// Provides access to the road tree graph. - public let roadGraph: RoadGraph - /// Provides access to metadata about road objects. - public let roadObjectStore: RoadObjectStore - /// Provides methods for road object matching. - public let roadObjectMatcher: RoadObjectMatcher -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RoadInfo.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RoadInfo.swift deleted file mode 100644 index d3f07b9b3..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RoadInfo.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// RoadInfo.swift -// -// -// Created by Maksim Chizhavko on 1/17/24. -// - -import Foundation -import MapboxDirections - -public struct RoadInfo: Equatable, Sendable { - /// the country code (ISO-2 format) of the road - public let countryCodeIso2: String? - - /// right-hand or left-hand traffic type - public let drivingSide: DrivingSide - - /// true if current road is one-way. - public let isOneWay: Bool - - /// the number of lanes - public let laneCount: Int? - - /// The edge’s general road classes. - public let roadClasses: RoadClasses - - public init( - countryCodeIso2: String?, - drivingSide: DrivingSide, - isOneWay: Bool, - laneCount: Int?, - roadClasses: RoadClasses - ) { - self.countryCodeIso2 = countryCodeIso2 - self.drivingSide = drivingSide - self.isOneWay = isOneWay - self.laneCount = laneCount - self.roadClasses = roadClasses - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteLegProgress.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteLegProgress.swift deleted file mode 100644 index b4723c268..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteLegProgress.swift +++ /dev/null @@ -1,231 +0,0 @@ -import CoreLocation -import Foundation -import MapboxDirections -import MapboxNavigationNative - -/// ``RouteLegProgress`` stores the user’s progress along a route leg. -public struct RouteLegProgress: Equatable, Sendable { - // MARK: Details About the Leg - - mutating func update(using status: NavigationStatus) { - guard let activeGuidanceInfo = status.activeGuidanceInfo else { - return - } - - let statusStepIndex = Int(status.stepIndex) - guard leg.steps.indices ~= statusStepIndex else { - Log.error("Incorrect step index update: \(statusStepIndex)", category: .navigation) - return - } - - if stepIndex == statusStepIndex { - currentStepProgress.update(using: status) - } else { - var stepProgress = RouteStepProgress(step: leg.steps[statusStepIndex]) - stepProgress.update(using: status) - currentStepProgress = stepProgress - } - - stepIndex = statusStepIndex - shapeIndex = Int(status.shapeIndex) - - currentSpeedLimit = nil - if let speed = status.speedLimit.speed?.doubleValue { - switch status.speedLimit.localeUnit { - case .milesPerHour: - currentSpeedLimit = Measurement(value: speed, unit: .milesPerHour) - case .kilometresPerHour: - currentSpeedLimit = Measurement(value: speed, unit: .kilometersPerHour) - @unknown default: - assertionFailure("Unknown native speed limit unit.") - } - } - - distanceTraveled = activeGuidanceInfo.legProgress.distanceTraveled - durationRemaining = activeGuidanceInfo.legProgress.remainingDuration - distanceRemaining = activeGuidanceInfo.legProgress.remainingDistance - fractionTraveled = activeGuidanceInfo.legProgress.fractionTraveled - - if remainingSteps.count <= 2, status.routeState == .complete { - userHasArrivedAtWaypoint = true - } - } - - /// Returns the current ``RouteLeg``. - public private(set) var leg: RouteLeg - - /// Total distance traveled in meters along current leg. - public private(set) var distanceTraveled: CLLocationDistance = 0 - - /// Duration remaining in seconds on current leg. - public private(set) var durationRemaining: TimeInterval = 0 - - /// Distance remaining on the current leg. - public private(set) var distanceRemaining: CLLocationDistance = 0 - - /// Number between 0 and 1 representing how far along the current leg the user has traveled. - public private(set) var fractionTraveled: Double = 0 - - public var userHasArrivedAtWaypoint = false - - // MARK: Details About the Leg’s Steps - - /// Index representing the current step. - public private(set) var stepIndex: Int = 0 - - /// The remaining steps for user to complete. - public var remainingSteps: [RouteStep] { - return Array(leg.steps.suffix(from: stepIndex + 1)) - } - - /// Returns the ``RouteStep`` before a given step. Returns `nil` if there is no step prior. - public func stepBefore(_ step: RouteStep) -> RouteStep? { - guard let index = leg.steps.firstIndex(of: step) else { - return nil - } - if index > 0 { - return leg.steps[index - 1] - } - return nil - } - - /// Returns the ``RouteStep`` after a given step. Returns `nil` if there is not a step after. - public func stepAfter(_ step: RouteStep) -> RouteStep? { - guard let index = leg.steps.firstIndex(of: step) else { - return nil - } - if index + 1 < leg.steps.endIndex { - return leg.steps[index + 1] - } - return nil - } - - /// Returns the ``RouteStep`` before the current step. - /// - /// If there is no ``priorStep``, `nil` is returned. - public var priorStep: RouteStep? { - guard stepIndex - 1 >= 0 else { - return nil - } - return leg.steps[stepIndex - 1] - } - - /// Returns the current ``RouteStep`` for the leg the user is on. - public var currentStep: RouteStep { - return leg.steps[stepIndex] - } - - /// Returns the ``RouteStep`` after the current step. - /// - /// If there is no ``upcomingStep``, `nil` is returned. - public var upcomingStep: RouteStep? { - guard stepIndex + 1 < leg.steps.endIndex else { - return nil - } - return leg.steps[stepIndex + 1] - } - - /// Returns step 2 steps ahead. - /// - /// If there is no ``followOnStep``, `nil` is returned. - public var followOnStep: RouteStep? { - guard stepIndex + 2 < leg.steps.endIndex else { - return nil - } - return leg.steps[stepIndex + 2] - } - - /// Return bool whether step provided is the current ``RouteStep`` the user is on. - public func isCurrentStep(_ step: RouteStep) -> Bool { - return step == currentStep - } - - /// Returns the progress along the current ``RouteStep``. - public internal(set) var currentStepProgress: RouteStepProgress - - /// Returns the SpeedLimit for the current position along the route. Returns SpeedLimit.invalid if the speed limit - /// is unknown or missing. - /// - /// The maximum speed may be an advisory speed limit for segments where legal limits are not posted, such as highway - /// entrance and exit ramps. If the speed limit along a particular segment is unknown, it is set to `nil`. If the - /// speed is unregulated along the segment, such as on the German _Autobahn_ system, it is represented by a - /// measurement whose value is `Double.infinity`. - /// - /// Speed limit data is available in [a number of countries and territories - /// worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/). - public private(set) var currentSpeedLimit: Measurement? = nil - - /// Index relative to leg shape, representing the point the user is currently located at. - public private(set) var shapeIndex: Int = 0 - - /// Intializes a new ``RouteLegProgress``. - /// - Parameter leg: Leg on a ``NavigationRoute``. - public init(leg: RouteLeg) { - precondition( - leg.steps.indices.contains(stepIndex), - "It's not possible to set the stepIndex: \(stepIndex) when it's higher than steps count \(leg.steps.count) or not included." - ) - - self.leg = leg - - self.currentStepProgress = RouteStepProgress(step: leg.steps[stepIndex]) - } - - func refreshingLeg(with leg: RouteLeg) -> RouteLegProgress { - var refreshedProgress = self - - refreshedProgress.leg = leg - refreshedProgress.currentStepProgress = refreshedProgress.currentStepProgress - .refreshingStep(with: leg.steps[stepIndex]) - - return refreshedProgress - } - - typealias StepIndexDistance = (index: Int, distance: CLLocationDistance) - - func closestStep(to coordinate: CLLocationCoordinate2D) -> StepIndexDistance? { - var currentClosest: StepIndexDistance? - let remainingSteps = leg.steps.suffix(from: stepIndex) - - for (currentStepIndex, step) in remainingSteps.enumerated() { - guard let shape = step.shape else { continue } - guard let closestCoordOnStep = shape.closestCoordinate(to: coordinate) else { continue } - let closesCoordOnStepDistance = closestCoordOnStep.coordinate.distance(to: coordinate) - let foundIndex = currentStepIndex + stepIndex - - // First time around, currentClosest will be `nil`. - guard let currentClosestDistance = currentClosest?.distance else { - currentClosest = (index: foundIndex, distance: closesCoordOnStepDistance) - continue - } - - if closesCoordOnStepDistance < currentClosestDistance { - currentClosest = (index: foundIndex, distance: closesCoordOnStepDistance) - } - } - - return currentClosest - } - - /// The waypoints remaining on the current leg, not including the leg’s destination. - func remainingWaypoints(among waypoints: [MapboxDirections.Waypoint]) -> [MapboxDirections.Waypoint] { - guard waypoints.count > 1 else { - // The leg has only a source and no via points. Save ourselves a call to RouteLeg.coordinates, which can be - // expensive. - return [] - } - let legPolyline = leg.shape - guard let userCoordinateIndex = legPolyline.indexedCoordinateFromStart(distance: distanceTraveled)?.index else { - // The leg is empty, so none of the waypoints are meaningful. - return [] - } - var slice = legPolyline - var accumulatedCoordinates = 0 - return Array(waypoints.drop { waypoint -> Bool in - let newSlice = slice.sliced(from: waypoint.coordinate)! - accumulatedCoordinates += slice.coordinates.count - newSlice.coordinates.count - slice = newSlice - return accumulatedCoordinates <= userCoordinateIndex - }) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteProgress.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteProgress.swift deleted file mode 100644 index bd213370d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/RouteProgress/RouteProgress.swift +++ /dev/null @@ -1,419 +0,0 @@ -import CoreLocation -import Foundation -import MapboxDirections -import class MapboxNavigationNative.NavigationStatus -import class MapboxNavigationNative.UpcomingRouteAlert -import Turf - -/// ``RouteProgress`` stores the user’s progress along a route. -public struct RouteProgress: Equatable, Sendable { - private static let reroutingAccuracy: CLLocationAccuracy = 90 - - /// Initializes a new ``RouteProgress``. - /// - Parameters: - /// - navigationRoutes: The selection of routes to follow. - /// - waypoints: The waypoints of the routes. - /// - congestionConfiguration: The congestion configuration to use to display the routes. - public init( - navigationRoutes: NavigationRoutes, - waypoints: [Waypoint], - congestionConfiguration: CongestionRangesConfiguration = .default - ) { - self.navigationRoutes = navigationRoutes - self.waypoints = waypoints - - self.currentLegProgress = RouteLegProgress(leg: navigationRoutes.mainRoute.route.legs[legIndex]) - - self.routeAlerts = routeAlerts(from: navigationRoutes.mainRoute) - calculateLegsCongestion(configuration: congestionConfiguration) - } - - /// Current `RouteOptions`, optimized for rerouting. - /// - /// This method is useful for implementing custom rerouting. Resulting `RouteOptions` skip passed waypoints and - /// include current user heading if possible. - /// - Parameters: - /// - location: Current user location. Treated as route origin for rerouting. - /// - routeOptions: The initial `RouteOptions`. - /// - Returns: Modified `RouteOptions`. - func reroutingOptions(from location: CLLocation, routeOptions: RouteOptions) -> RouteOptions { - let oldOptions = routeOptions - var user = Waypoint(coordinate: location.coordinate) - - // A pedestrian can turn on a dime; there's no problem with a route that starts out by turning the pedestrian - // around. - let transportType = currentLegProgress.currentStep.transportType - if transportType != .walking, location.course >= 0 { - user.heading = location.course - user.headingAccuracy = RouteProgress.reroutingAccuracy - } - let newWaypoints = [user] + remainingWaypointsForCalculatingRoute() - let newOptions: RouteOptions - do { - newOptions = try oldOptions.copy() - } catch { - newOptions = oldOptions - } - newOptions.waypoints = newWaypoints - - return newOptions - } - - // MARK: Route Statistics - - mutating func update(using status: NavigationStatus) { - guard let activeGuidanceInfo = status.activeGuidanceInfo else { - return - } - - if legIndex == Int(status.legIndex) { - currentLegProgress.update(using: status) - } else { - let legIndex = Int(status.legIndex) - guard route.legs.indices.contains(legIndex) else { - Log.info("Ignoring incorrect status update with leg index \(legIndex)", category: .navigation) - return - } - var leg = RouteLegProgress(leg: route.legs[legIndex]) - leg.update(using: status) - currentLegProgress = leg - } - - upcomingRouteAlerts = status.upcomingRouteAlertUpdates.compactMap { routeAlert in - routeAlerts[routeAlert.id].map { - RouteAlert($0, distanceToStart: routeAlert.distanceToStart) - } - } - shapeIndex = Int(status.geometryIndex) - legIndex = Int(status.legIndex) - - updateDistanceToIntersection() - - distanceTraveled = activeGuidanceInfo.routeProgress.distanceTraveled - durationRemaining = activeGuidanceInfo.routeProgress.remainingDuration - fractionTraveled = activeGuidanceInfo.routeProgress.fractionTraveled - distanceRemaining = activeGuidanceInfo.routeProgress.remainingDistance - } - - mutating func updateAlternativeRoutes(using navigationRoutes: NavigationRoutes) { - guard self.navigationRoutes.mainRoute == navigationRoutes.mainRoute, - self.navigationRoutes.alternativeRoutes.map(\.routeId) != navigationRoutes.alternativeRoutes - .map(\.routeId) - else { - return - } - self.navigationRoutes = navigationRoutes - } - - func refreshingRoute( - with refreshedRoutes: NavigationRoutes, - legIndex: Int, - legShapeIndex: Int, - congestionConfiguration: CongestionRangesConfiguration - ) -> RouteProgress { - var refreshedRouteProgress = self - - refreshedRouteProgress.routeAlerts = routeAlerts(from: refreshedRoutes.mainRoute) - - refreshedRouteProgress.navigationRoutes = refreshedRoutes - - refreshedRouteProgress.currentLegProgress = refreshedRouteProgress.currentLegProgress - .refreshingLeg(with: refreshedRouteProgress.route.legs[legIndex]) - refreshedRouteProgress.calculateLegsCongestion(configuration: congestionConfiguration) - - return refreshedRouteProgress - } - - public let waypoints: [Waypoint] - - /// Total distance traveled by user along all legs. - public private(set) var distanceTraveled: CLLocationDistance = 0 - - /// Total seconds remaining on all legs. - public private(set) var durationRemaining: TimeInterval = 0 - - /// Number between 0 and 1 representing how far along the `Route` the user has traveled. - public private(set) var fractionTraveled: Double = 0 - - /// Total distance remaining in meters along route. - public private(set) var distanceRemaining: CLLocationDistance = 0 - - /// The waypoints remaining on the current route. - /// - /// This property does not include waypoints whose `Waypoint.separatesLegs` property is set to `false`. - public var remainingWaypoints: [Waypoint] { - return route.legs.suffix(from: legIndex).compactMap(\.destination) - } - - func waypoints(fromLegAt legIndex: Int) -> ([Waypoint], [Waypoint]) { - // The first and last waypoints always separate legs. Make exceptions for these waypoints instead of modifying - // them by side effect. - let legSeparators = waypoints.filterKeepingFirstAndLast { $0.separatesLegs } - let viaPointsByLeg = waypoints.splitExceptAtStartAndEnd(omittingEmptySubsequences: false) { $0.separatesLegs } - .dropFirst() // No leg precedes first separator. - - let reconstitutedWaypoints = zip(legSeparators, viaPointsByLeg).dropFirst(legIndex).map { [$0.0] + $0.1 } - let legWaypoints = reconstitutedWaypoints.first ?? [] - let subsequentWaypoints = reconstitutedWaypoints.dropFirst() - return (legWaypoints, subsequentWaypoints.flatMap { $0 }) - } - - /// The waypoints remaining on the current route, including any waypoints that do not separate legs. - func remainingWaypointsForCalculatingRoute() -> [Waypoint] { - let (currentLegViaPoints, remainingWaypoints) = waypoints(fromLegAt: legIndex) - let currentLegRemainingViaPoints = currentLegProgress.remainingWaypoints(among: currentLegViaPoints) - return currentLegRemainingViaPoints + remainingWaypoints - } - - /// Upcoming ``RouteAlert``s as reported by the navigation engine. - /// - /// The contents of the array depend on user's current progress along the route and are modified on each location - /// update. This array contains only the alerts that the user has not passed. Some events may have non-zero length - /// and are also included while the user is traversing it. You can use this property to get information about - /// incoming points of interest. - public private(set) var upcomingRouteAlerts: [RouteAlert] = [] - private(set) var routeAlerts: [String: UpcomingRouteAlert] = [:] - - private func routeAlerts(from navigationRoute: NavigationRoute) -> [String: UpcomingRouteAlert] { - return navigationRoute.nativeRoute.getRouteInfo().alerts.reduce(into: [:]) { partialResult, alert in - partialResult[alert.roadObject.id] = alert - } - } - - /// Returns an array of `CLLocationCoordinate2D` of the coordinates along the current step and any adjacent steps. - /// - /// - Important: The adjacent steps may be part of legs other than the current leg. - public var nearbyShape: LineString { - let priorCoordinates = priorStep?.shape?.coordinates.dropLast() ?? [] - let currentShape = currentLegProgress.currentStep.shape - let upcomingCoordinates = upcomingStep?.shape?.coordinates.dropFirst() ?? [] - if let currentShape, priorCoordinates.isEmpty, upcomingCoordinates.isEmpty { - return currentShape - } - return LineString(priorCoordinates + (currentShape?.coordinates ?? []) + upcomingCoordinates) - } - - // MARK: Updating the RouteProgress - - /// Returns the current ``NavigationRoutes``. - public private(set) var navigationRoutes: NavigationRoutes - - /// Returns the current main `Route`. - public var route: Route { - navigationRoutes.mainRoute.route - } - - public var routeId: RouteId { - navigationRoutes.mainRoute.routeId - } - - /// Index relative to route shape, representing the point the user is currently located at. - public private(set) var shapeIndex: Int = 0 - - /// Update the distance to intersection according to new location specified. - private mutating func updateDistanceToIntersection() { - guard var intersections = currentLegProgress.currentStepProgress.step.intersections else { return } - - // The intersections array does not include the upcoming maneuver intersection. - if let upcomingIntersection = currentLegProgress.upcomingStep?.intersections?.first { - intersections += [upcomingIntersection] - } - currentLegProgress.currentStepProgress.update(intersectionsIncludingUpcomingManeuverIntersection: intersections) - - if let shape = currentLegProgress.currentStep.shape, - let upcomingIntersection = currentLegProgress.currentStepProgress.upcomingIntersection, - let coordinateOnStep = shape.coordinateFromStart( - distance: currentLegProgress.currentStepProgress.distanceTraveled - ) - { - currentLegProgress.currentStepProgress.userDistanceToUpcomingIntersection = shape.distance( - from: coordinateOnStep, - to: upcomingIntersection.location - ) - } - } - - // MARK: Leg Statistics - - /// Index representing current ``RouteLeg``. - public private(set) var legIndex: Int = 0 - - /// If waypoints are provided in the `Route`, this will contain which leg the user is on. - public var currentLeg: RouteLeg { - return route.legs[legIndex] - } - - /// Returns the remaining legs left on the current route - public var remainingLegs: [RouteLeg] { - return Array(route.legs.suffix(from: legIndex + 1)) - } - - /// Returns true if ``currentLeg`` is the last leg. - public var isFinalLeg: Bool { - guard let lastLeg = route.legs.last else { return false } - return currentLeg == lastLeg - } - - /// Returns the progress along the current ``RouteLeg``. - public var currentLegProgress: RouteLegProgress - - /// The previous leg. - public var priorLeg: RouteLeg? { - return legIndex > 0 ? route.legs[legIndex - 1] : nil - } - - /// The leg following the current leg along this route. - /// - /// If this leg is the last leg of the route, this property is set to nil. - public var upcomingLeg: RouteLeg? { - return legIndex + 1 < route.legs.endIndex ? route.legs[legIndex + 1] : nil - } - - // MARK: Step Statistics - - /// Returns the remaining steps left on the current route - public var remainingSteps: [RouteStep] { - return currentLegProgress.remainingSteps + remainingLegs.flatMap(\.steps) - } - - /// The step prior to the current step along this route. - /// - /// The prior step may be part of a different RouteLeg than the current step. If the current step is the first step - /// along the route, this property is set to nil. - public var priorStep: RouteStep? { - return currentLegProgress.priorStep ?? priorLeg?.steps.last - } - - /// The step following the current step along this route. - /// - /// The upcoming step may be part of a different ``RouteLeg`` than the current step. If it is the last step along - /// the route, this property is set to nil. - public var upcomingStep: RouteStep? { - return currentLegProgress.upcomingStep ?? upcomingLeg?.steps.first - } - - // MARK: Leg Attributes - - /// The struc containing a ``CongestionLevel`` and a corresponding `TimeInterval` representing the expected travel - /// time for this segment. - public struct TimedCongestionLevel: Equatable, Sendable { - public var level: CongestionLevel - public var timeInterval: TimeInterval - } - - /// If the route contains both `RouteLeg.segmentCongestionLevels` and `RouteLeg.expectedSegmentTravelTimes`, this - /// property is set - /// to a deeply nested array of ``RouteProgress/TimedCongestionLevel`` per segment per step per leg. - public private(set) var congestionTravelTimesSegmentsByStep: [[[TimedCongestionLevel]]] = [] - - /// An dictionary containing a `TimeInterval` total per ``CongestionLevel``. Only ``CongestionLevel`` found on that - /// step will present. Broken up by leg and then step. - public private(set) var congestionTimesPerStep: [[[CongestionLevel: TimeInterval]]] = [[[:]]] - - public var averageCongestionLevelRemainingOnLeg: CongestionLevel? { - guard let coordinates = currentLegProgress.currentStepProgress.step.shape?.coordinates else { - return .unknown - } - - let coordinatesLeftOnStepCount = - Int(floor(Double(coordinates.count) * currentLegProgress.currentStepProgress.fractionTraveled)) - - guard coordinatesLeftOnStepCount >= 0 else { return .unknown } - - guard legIndex < congestionTravelTimesSegmentsByStep.count, - currentLegProgress.stepIndex < congestionTravelTimesSegmentsByStep[legIndex].count - else { return .unknown } - - let congestionTimesForStep = congestionTravelTimesSegmentsByStep[legIndex][currentLegProgress.stepIndex] - guard coordinatesLeftOnStepCount <= congestionTimesForStep.count else { return .unknown } - - let remainingCongestionTimesForStep = congestionTimesForStep.suffix(from: coordinatesLeftOnStepCount) - let remainingCongestionTimesForRoute = congestionTimesPerStep[legIndex] - .suffix(from: currentLegProgress.stepIndex + 1) - - var remainingStepCongestionTotals: [CongestionLevel: TimeInterval] = [:] - for stepValues in remainingCongestionTimesForRoute { - for (key, value) in stepValues { - remainingStepCongestionTotals[key] = (remainingStepCongestionTotals[key] ?? 0) + value - } - } - - for remainingCongestionTimeForStep in remainingCongestionTimesForStep { - let segmentCongestion = remainingCongestionTimeForStep.level - let segmentTime = remainingCongestionTimeForStep.timeInterval - remainingStepCongestionTotals[segmentCongestion] = (remainingStepCongestionTotals[segmentCongestion] ?? 0) + - segmentTime - } - - if durationRemaining < 60 { - return .unknown - } else { - if let max = remainingStepCongestionTotals.max(by: { a, b in a.value < b.value }) { - return max.key - } else { - return .unknown - } - } - } - - mutating func calculateLegsCongestion(configuration: CongestionRangesConfiguration) { - congestionTimesPerStep.removeAll() - congestionTravelTimesSegmentsByStep.removeAll() - - for (legIndex, leg) in route.legs.enumerated() { - var maneuverCoordinateIndex = 0 - - congestionTimesPerStep.append([]) - - /// An index into the route’s coordinates and congestionTravelTimesSegmentsByStep that corresponds to a - /// step’s maneuver location. - var congestionTravelTimesSegmentsByLeg: [[TimedCongestionLevel]] = [] - - if let segmentCongestionLevels = leg.resolveCongestionLevels(using: configuration), - let expectedSegmentTravelTimes = leg.expectedSegmentTravelTimes - { - for step in leg.steps { - guard let coordinates = step.shape?.coordinates else { continue } - let stepCoordinateCount = step.maneuverType == .arrive ? Int(coordinates.count) : coordinates - .dropLast().count - let nextManeuverCoordinateIndex = maneuverCoordinateIndex + stepCoordinateCount - 1 - - guard nextManeuverCoordinateIndex < segmentCongestionLevels.count else { continue } - guard nextManeuverCoordinateIndex < expectedSegmentTravelTimes.count else { continue } - - let stepSegmentCongestionLevels = - Array(segmentCongestionLevels[maneuverCoordinateIndex.. RouteStepProgress { - var refreshedProgress = self - - refreshedProgress.step = step - - return refreshedProgress - } - - // MARK: Step Stats - - mutating func update(using status: NavigationStatus) { - guard let activeGuidanceInfo = status.activeGuidanceInfo else { - return - } - - distanceTraveled = activeGuidanceInfo.stepProgress.distanceTraveled - distanceRemaining = activeGuidanceInfo.stepProgress.remainingDistance - fractionTraveled = activeGuidanceInfo.stepProgress.fractionTraveled - durationRemaining = activeGuidanceInfo.stepProgress.remainingDuration - - intersectionIndex = Int(status.intersectionIndex) - visualInstructionIndex = status.bannerInstruction.map { Int($0.index) } ?? visualInstructionIndex - // TODO: ensure NN fills these only when it is really needed (mind reroutes/alternatives switch/etc) - spokenInstructionIndex = status.voiceInstruction.map { Int($0.index) } - currentSpokenInstruction = status.voiceInstruction.map(SpokenInstruction.init) - } - - /// Returns the current ``RouteStep``. - public private(set) var step: RouteStep - - /// Returns distance user has traveled along current step. - public private(set) var distanceTraveled: CLLocationDistance = 0 - - /// Total distance in meters remaining on current step. - public private(set) var distanceRemaining: CLLocationDistance = 0 - - /// Number between 0 and 1 representing fraction of current step traveled. - public private(set) var fractionTraveled: Double = 0 - - /// Number of seconds remaining on current step. - public private(set) var durationRemaining: TimeInterval = 0 - - /// Returns remaining step shape coordinates. - public func remainingStepCoordinates() -> [CLLocationCoordinate2D] { - guard let shape = step.shape else { - return [] - } - - guard let indexedStartCoordinate = shape.indexedCoordinateFromStart(distance: distanceTraveled) else { - return [] - } - - return Array(shape.coordinates.suffix(from: indexedStartCoordinate.index)) - } - - // MARK: Intersections - - /// All intersections on the current ``RouteStep`` and also the first intersection on the upcoming ``RouteStep``. - /// - /// The upcoming RouteStep first Intersection is added because it is omitted from the current step. - public var intersectionsIncludingUpcomingManeuverIntersection: [Intersection]? - - mutating func update(intersectionsIncludingUpcomingManeuverIntersection newValue: [Intersection]?) { - intersectionsIncludingUpcomingManeuverIntersection = newValue - } - - /// The next intersection the user will travel through. - /// The step must contain ``intersectionsIncludingUpcomingManeuverIntersection`` otherwise this property will be - /// `nil`. - public var upcomingIntersection: Intersection? { - guard let intersections = intersectionsIncludingUpcomingManeuverIntersection, intersections.count > 0, - intersections.startIndex.. { - return ["step.instructionsDisplayedAlongStep", "visualInstructionIndex"] - } - - public var keyPathsAffectingValueForRemainingSpokenInstructions: Set { - return ["step.instructionsDisplayedAlongStep", "spokenInstructionIndex"] - } -} - -extension SpokenInstruction { - init(_ nativeInstruction: VoiceInstruction) { - self.init( - distanceAlongStep: LocationDistance(nativeInstruction.remainingStepDistance), // is it the same distance? - text: nativeInstruction.announcement, - ssmlText: nativeInstruction.ssmlAnnouncement - ) - } -} - -extension VisualInstructionBanner { - init(_ nativeInstruction: BannerInstruction) { - let drivingSide: DrivingSide = if let nativeDrivingSide = nativeInstruction.primary.drivingSide, - let converted = DrivingSide(rawValue: nativeDrivingSide) - { - converted - } else { - .right - } - - self.init( - distanceAlongStep: LocationDistance(nativeInstruction.remainingStepDistance), - primary: .init(nativeInstruction.primary), - secondary: nativeInstruction.secondary.map(VisualInstruction.init), - tertiary: nativeInstruction.sub.map(VisualInstruction.init), - quaternary: nativeInstruction.view.map(VisualInstruction.init), - drivingSide: drivingSide - ) - } -} - -extension VisualInstruction { - init(_ nativeInstruction: BannerSection) { - let maneuverType = nativeInstruction.type.map(ManeuverType.init(rawValue:)) ?? nil - let maneuverDirection = nativeInstruction.modifier.map(ManeuverDirection.init(rawValue:)) ?? nil - let components = nativeInstruction.components?.map(VisualInstruction.Component.init) ?? [] - - self.init( - text: nativeInstruction.text, - maneuverType: maneuverType, - maneuverDirection: maneuverDirection, - components: components, - degrees: nativeInstruction.degrees?.doubleValue - ) - } -} - -extension VisualInstruction.Component { - init(_ nativeComponent: BannerComponent) { - let textRepresentation = TextRepresentation( - text: nativeComponent.text, - abbreviation: nativeComponent.abbr, - abbreviationPriority: nativeComponent.abbrPriority?.intValue - ) - // TODO: get rid of constants - switch nativeComponent.type { - case "delimeter": - self = .delimiter(text: textRepresentation) - return - case "text": - self = .text(text: textRepresentation) - return - case "image": - guard let nativeShield = nativeComponent.shield, - let baseURL = URL(string: nativeShield.baseUrl), - let imageBaseURL = nativeComponent.imageBaseUrl - else { - break - } - let shield = ShieldRepresentation( - baseURL: baseURL, - name: nativeShield.name, - textColor: nativeShield.textColor, - text: nativeShield.displayRef - ) - self = .image( - image: ImageRepresentation( - imageBaseURL: URL(string: imageBaseURL), - shield: shield - ), - alternativeText: textRepresentation - ) - return - case "guidance-view": - guard let imageURL = nativeComponent.imageURL else { - break - } - self = .guidanceView( - image: GuidanceViewImageRepresentation(imageURL: URL(string: imageURL)), - alternativeText: textRepresentation - ) - return - case "exit": - self = .exit(text: textRepresentation) - return - case "exit-number": - self = .exitCode(text: textRepresentation) - return - case "lane": - guard let directions = nativeComponent.directions, - let indications = LaneIndication(descriptions: directions) - else { - break - } - let activeDirection = nativeComponent.activeDirection - let preferredDirection = activeDirection.flatMap { ManeuverDirection(rawValue: $0) } - self = .lane( - indications: indications, - isUsable: nativeComponent.active?.boolValue ?? false, - preferredDirection: preferredDirection - ) - return - default: - break - } - self = .text(text: textRepresentation) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/SpeedLimit.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/SpeedLimit.swift deleted file mode 100644 index 989b3da7b..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/SpeedLimit.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation -import MapboxDirections - -public struct SpeedLimit: Equatable, @unchecked Sendable { - public let value: Measurement? - public let signStandard: SignStandard -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Tunnel.swift b/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Tunnel.swift deleted file mode 100644 index 6154b5bbc..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Navigator/Tunnel.swift +++ /dev/null @@ -1,13 +0,0 @@ - -import Foundation -import MapboxNavigationNative - -/// ``Tunnel`` is used for naming incoming tunnels, together with route alerts. -public struct Tunnel: Equatable { - /// The name of the tunnel. - public let name: String? - - init(_ tunnelInfo: TunnelInfo) { - self.name = tunnelInfo.name - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheConfig.swift deleted file mode 100644 index ae7119d09..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheConfig.swift +++ /dev/null @@ -1,29 +0,0 @@ -import MapboxDirections -import MapboxNavigationNative - -/// Specifies the content that a predictive cache fetches and how it fetches the content. -public struct PredictiveCacheConfig: Equatable, Sendable { - /// Predictive cache Navigation related config - public var predictiveCacheNavigationConfig: PredictiveCacheNavigationConfig = .init() - - /// Predictive cache Map related config - public var predictiveCacheMapsConfig: PredictiveCacheMapsConfig = .init() - - /// Predictive cache Search domain related config - public var predictiveCacheSearchConfig: PredictiveCacheSearchConfig? = nil - - /// Creates a new `PredictiveCacheConfig` instance. - /// - Parameters: - /// - predictiveCacheNavigationConfig: Navigation related config. - /// - predictiveCacheMapsConfig: Map related config. - /// - predictiveCacheSearchConfig: Search related config - public init( - predictiveCacheNavigationConfig: PredictiveCacheNavigationConfig = .init(), - predictiveCacheMapsConfig: PredictiveCacheMapsConfig = .init(), - predictiveCacheSearchConfig: PredictiveCacheSearchConfig? = nil - ) { - self.predictiveCacheNavigationConfig = predictiveCacheNavigationConfig - self.predictiveCacheMapsConfig = predictiveCacheMapsConfig - self.predictiveCacheSearchConfig = predictiveCacheSearchConfig - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheLocationConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheLocationConfig.swift deleted file mode 100644 index e2d59ce83..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheLocationConfig.swift +++ /dev/null @@ -1,39 +0,0 @@ -import MapboxNavigationNative - -/// Specifies the content that a predictive cache fetches and how it fetches the content. -public struct PredictiveCacheLocationConfig: Equatable, Sendable { - /// How far around the user's location caching is going to be performed. - /// - /// Defaults to 2000 meters. - public var currentLocationRadius: CLLocationDistance = 2000 - - /// How far around the active route caching is going to be performed (if route is set). - /// - /// Defaults to 500 meters. - public var routeBufferRadius: CLLocationDistance = 500 - - /// How far around the destination location caching is going to be performed (if route is set). - /// - /// Defaults to 5000 meters. - public var destinationLocationRadius: CLLocationDistance = 5000 - - public init( - currentLocationRadius: CLLocationDistance = 2000, - routeBufferRadius: CLLocationDistance = 500, - destinationLocationRadius: CLLocationDistance = 5000 - ) { - self.currentLocationRadius = currentLocationRadius - self.routeBufferRadius = routeBufferRadius - self.destinationLocationRadius = destinationLocationRadius - } -} - -extension PredictiveLocationTrackerOptions { - convenience init(_ locationOptions: PredictiveCacheLocationConfig) { - self.init( - currentLocationRadius: UInt32(locationOptions.currentLocationRadius), - routeBufferRadius: UInt32(locationOptions.routeBufferRadius), - destinationLocationRadius: UInt32(locationOptions.destinationLocationRadius) - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheManager.swift deleted file mode 100644 index 54ea5903a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheManager.swift +++ /dev/null @@ -1,111 +0,0 @@ -import Foundation -import MapboxCommon -import MapboxMaps -import MapboxNavigationNative - -/// Proactively fetches tiles which may become necessary if the device loses its Internet connection at some point -/// during passive or active turn-by-turn navigation. -/// -/// Typically, you initialize an instance of this class and retain it as long as caching is required. Pass -/// ``MapboxNavigationProvider/predictiveCacheManager`` to your ``NavigationMapView`` instance to use predictive cache. -/// - Note: This object uses global tile store configuration from ``CoreConfig/predictiveCacheConfig``. -public class PredictiveCacheManager { - private let predictiveCacheOptions: PredictiveCacheConfig - private let tileStore: TileStore - - private weak var navigator: NavigationNativeNavigator? { - didSet { - _ = mapTilesetDescriptor.map { descriptor in - Task { @MainActor in - self.mapController = createMapController(descriptor) - } - } - } - } - - private var mapTilesetDescriptor: TilesetDescriptor? - private var navigationController: PredictiveCacheController? - private var mapController: PredictiveCacheController? - private var searchController: PredictiveCacheController? - - init( - predictiveCacheOptions: PredictiveCacheConfig, - tileStore: TileStore, - styleSourcePaths: [String] = [] - ) { - self.predictiveCacheOptions = predictiveCacheOptions - self.tileStore = tileStore - } - - @MainActor - public func updateMapControllers(mapView: MapView) { - let mapsOptions = predictiveCacheOptions.predictiveCacheMapsConfig - let tilesetDescriptor = mapView.tilesetDescriptor(zoomRange: mapsOptions.zoomRange) - mapTilesetDescriptor = tilesetDescriptor - mapController = createMapController(tilesetDescriptor) - } - - @MainActor - func updateNavigationController(with navigator: NavigationNativeNavigator?) { - self.navigator = navigator - navigationController = createNavigationController(for: navigator) - } - - @MainActor - func updateSearchController(with navigator: NavigationNativeNavigator?) { - self.navigator = navigator - searchController = createSearchController(for: navigator) - } - - @MainActor - private func createMapController(_ tilesetDescriptor: TilesetDescriptor?) -> PredictiveCacheController? { - guard let tilesetDescriptor else { return nil } - - let cacheMapsOptions = predictiveCacheOptions.predictiveCacheMapsConfig - let predictiveLocationTrackerOptions = PredictiveLocationTrackerOptions(cacheMapsOptions.locationConfig) - return navigator?.native.createPredictiveCacheController( - for: tileStore, - descriptors: [tilesetDescriptor], - locationTrackerOptions: predictiveLocationTrackerOptions - ) - } - - @MainActor - private func createNavigationController( - for navigator: NavigationNativeNavigator? - ) -> PredictiveCacheController? { - guard let navigator else { return nil } - - let locationOptions = predictiveCacheOptions.predictiveCacheNavigationConfig.locationConfig - let predictiveLocationTrackerOptions = PredictiveLocationTrackerOptions(locationOptions) - return navigator.native.createPredictiveCacheController(for: predictiveLocationTrackerOptions) - } - - @MainActor - /// Instantiate a controller for search functionality if the ``PredictiveCacheSearchConfig`` has the necessary - /// inputs. - /// Assign `tileStore.setOptionForKey("log-tile-loading", value: true)` and set MapboxCommon log level to info for - /// debug output - /// - Returns: A predictive cache controller configured for search functionality. - private func createSearchController(for navigator: NavigationNativeNavigator?) -> PredictiveCacheController? { - guard let navigator, - let predictiveCacheSearchConfig = predictiveCacheOptions.predictiveCacheSearchConfig - else { - return nil - } - - let locationOptions = predictiveCacheSearchConfig.locationConfig - let predictiveLocationTrackerOptions = PredictiveLocationTrackerOptions(locationOptions) - - return navigator.native.createPredictiveCacheController( - for: tileStore, - descriptors: [ - predictiveCacheSearchConfig.searchTilesetDescriptor, - ], - locationTrackerOptions: predictiveLocationTrackerOptions - ) - } -} - -extension TilesetDescriptor: @unchecked Sendable {} -extension PredictiveCacheManager: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheMapsConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheMapsConfig.swift deleted file mode 100644 index 5e94e641c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheMapsConfig.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation - -/// Specifies predictive cache Maps related config. -public struct PredictiveCacheMapsConfig: Equatable, Sendable { - /// Location configuration for visual map predictive caching. - public var locationConfig: PredictiveCacheLocationConfig = .init() - - /// Maxiumum amount of concurrent requests, which will be used for caching. - /// Defaults to 2 concurrent requests. - public var maximumConcurrentRequests: UInt32 = 2 - - /// Closed range zoom level for the tile package. - /// See `TilesetDescriptorOptionsForTilesets.minZoom` and `TilesetDescriptorOptionsForTilesets.maxZoom`. - /// Defaults to 0..16. - public var zoomRange: ClosedRange = 0...16 - - /// Creates a new ``PredictiveCacheMapsConfig`` instance. - /// - Parameters: - /// - locationConfig: Location configuration for visual map predictive caching. - /// - maximumConcurrentRequests: Maxiumum amount of concurrent requests, which will be used for caching. - /// - zoomRange: Closed range zoom level for the tile package. - public init( - locationConfig: PredictiveCacheLocationConfig = .init(), - maximumConcurrentRequests: UInt32 = 2, - zoomRange: ClosedRange = 0...16 - ) { - self.locationConfig = locationConfig - self.maximumConcurrentRequests = maximumConcurrentRequests - self.zoomRange = zoomRange - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheNavigationConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheNavigationConfig.swift deleted file mode 100644 index 902a37faa..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheNavigationConfig.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -/// Specifies predictive cache Navigation related config. -public struct PredictiveCacheNavigationConfig: Equatable, Sendable { - /// Location configuration for predictive caching. - public var locationConfig: PredictiveCacheLocationConfig = .init() - - /// Creates a new ``PredictiveCacheNavigationConfig`` instance. - /// - Parameter locationConfig: Location configuration for predictive caching. - public init(locationConfig: PredictiveCacheLocationConfig = .init()) { - self.locationConfig = locationConfig - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheSearchConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheSearchConfig.swift deleted file mode 100644 index b9ee6008d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/PredictiveCache/PredictiveCacheSearchConfig.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import MapboxCommon - -/// Predictive cache search related options. -public struct PredictiveCacheSearchConfig: Equatable, Sendable { - /// Location configuration for visual map predictive caching. - public var locationConfig: PredictiveCacheLocationConfig = .init() - - /// TilesetDescriptor to use specifically for Search domain predictive cache. - /// Must be configured for Search tileset usage. - /// Required when used with `PredictiveCacheManager`. - public var searchTilesetDescriptor: TilesetDescriptor - - /// Create a new ``PredictiveCacheSearchConfig`` instance. - /// - Parameters: - /// - locationConfig: Location configuration for predictive caching. - /// - searchTilesetDescriptor: Required TilesetDescriptor for search domain predictive caching. - public init( - locationConfig: PredictiveCacheLocationConfig = .init(), - searchTilesetDescriptor: TilesetDescriptor - ) { - self.locationConfig = locationConfig - self.searchTilesetDescriptor = searchTilesetDescriptor - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/3DPuck.glb b/ios/Classes/Navigation/MapboxNavigationCore/Resources/3DPuck.glb deleted file mode 100644 index a7da8327173c474713a449c667e3a1a906b384f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31480 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 IFA1#nKX*YLxc~qF diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/Contents.json deleted file mode 100644 index f1f2e4e57..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "RailroadCrossing.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/RailroadCrossing.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/RailroadCrossing.imageset/RailroadCrossing.pdf deleted file mode 100644 index aa12b023f964aa59146aa653262a32f036929e6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14532 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 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/Contents.json deleted file mode 100644 index 433f9e4fa..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "YieldSign.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/YieldSign.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RoadIntersections/YieldSign.imageset/YieldSign.pdf deleted file mode 100644 index 36b6e28f8b6223ba7d5e777379a71bcaefbf2d9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10639 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 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/Contents.json deleted file mode 100644 index 0813d4704..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ra_accident.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/ra_accident.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_accident.imageset/ra_accident.pdf deleted file mode 100644 index a78a61d965a8b1c7f1322697150857859c9283c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18167 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%0DUxt4p5-HK2wRybH{I1~WF>9RPG9!pFJOBO7$ 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 AKL7v# diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/Contents.json deleted file mode 100644 index 09dcfecee..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ra_contruction.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/ra_contruction.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_construction.imageset/ra_contruction.pdf deleted file mode 100644 index 3edfe5fcbd43bfb1760216b2d7b7517ba275026d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15309 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 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/Contents.json deleted file mode 100644 index 0cd578422..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ra_disabled_vehicle.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/ra_disabled_vehicle.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_disabled_vehicle.imageset/ra_disabled_vehicle.pdf deleted file mode 100644 index 3928df0cd321a8c2d561dbb3f1a67119f62bfc78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16033 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 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/Contents.json deleted file mode 100644 index 770f037dc..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ra_lane_restriction.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/ra_lane_restriction.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_lane_restriction.imageset/ra_lane_restriction.pdf deleted file mode 100644 index 807aa89e1dae6bdba8c96ee4c67e5b81db16fab0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17449 zcmeHv2{e^m`}ac>$E;+?aLhv-XC6dmnKMKsWfobs!_ukj=yRLohYv1e0TNxOsVNfIl z0)s>&{ayDUkVs8Ur23Zq0Uk*8ZJS;9c({cC41CmWLtR6tu!*nBZVx)B=N%GkT zx+>!+lAp~gUMY=Dz+iT1?3E0;YvYPvcIyXkB+ggVFRCSOl!_j{94>RTGfdiD=9tZ6 zk===`_lU!e#>(VtI~1>9vQSo5tk1G~>9D&(ko$GJh!bf{zc+6eCECl#2RCFq!4Z~T zChBnE`z94i$!qh%2*SelD-KUyt60V>uj@FVUHR~Z@y#IZj>tL#QcIQJp?wLT2uB>_ zv}y;IY8;>DxZoPMuKQ~D`M}EQb{CYg(^ivvyB2<4<87l9;Y!A5vZkYbtsjWQ_@6_b z?@>J+IB@T}lmFFPv^`;S#(T39)UN7XKv; zY&PlrBNm+Ows<0j;Xmr)KP?c&uBWH!nJQt18Z--#dZW32L{P0hzTo}|`_OwW-`kK5 z3vHh{j|l!cT+*3@d0FOd$NvViYInG%(cO4>D!2n>l_Lii?mGE zQoaS<5$BCxjN0s9)&JVBMCgUgrmb?{=Azg(m!vlxsP4Jc*&{JuVRwt|<7b1YGW*Nt zRtJ~#cU?}GP#+WYOXI>_d!)WFGjpLhw4%h*Z7oKbTTQ?@gSGtl>KiJ%ZXVs+^o%>F zJ#M?f4iSfzQUC4bWx^Lc(&eMfk5vrZDpP-?>S`l$iI~}*#~yPhUw*rTJrB_&M}A9& zLWRMNBAObM9HA{Gf@h$ffZ^hu(A^oSm5N z4={bi$+y=l;YfM=a5ws_wtv-IX=K66O`SKleRWhQ^*OqCcfzI_!!62s=yO*OZCK;| zsQhjg`6d@zd48T~@8jt--L(dXmO50`E9OqvHAa}7=kmR_C)hIUa@gK!(+rXGi^gh? zUX5<|3g&#cNmcr7<=U6Oj#4_6tbejx@HKb(8)8FTqY)cVBErNb!05k~*hD7U(z6)} zL+2)q#_*s2hHMs@#pQNGlRK8=f9&`^c0+^vKEHlRMRk}sJeYjF�@yq9d(S=H2kH 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 HW~2WF*8IeH diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/Contents.json deleted file mode 100644 index 81904c333..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ra_mass_transit.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/ra_mass_transit.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_mass_transit.imageset/ra_mass_transit.pdf deleted file mode 100644 index 8d8d1eab0bb9905bc7dc1e0df425ef222ad28d38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15442 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 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/Contents.json deleted file mode 100644 index 75ecf82ab..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ra_miscellaneous.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/ra_miscellaneous.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_miscellaneous.imageset/ra_miscellaneous.pdf deleted file mode 100644 index 1d84cabd239b39da9ed050cc372cdc1538eda083..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11988 zcmcIqc|26@+fTBMy~vVf5TT6O7i-yNCnbb2wvl~HqJ@yi+JZuot)h@E>sT_f78Qxe zPKqKs?-@ir&-3}c@9*<@-t)(N&V284U-z|N*SXIvs;{CZ1%qHfAQ%`5cC~f{fx&We zVCfU*-E6_qCVJM6wgfK#11@PJZ)>j&SkoDAZ@UF55xqS1Z9SA-o!wkrY+by-*sTF= zTNisT2QVBUYq$kz*b?m>yub+9*8fvP8_E;Vt-s2yPOctCZg>K)vWl$_kzlLtfj_@B zs77@1vh@I)fu+@)@LskmwggujTksiRR6zmc>E&UIcLw=VdB1+|5dvnT>0g!$FMhxFAn5cRG-dIr5Jf)^%wAQWsy6j8t%zXq*AeKu>LkR08o^bQ{S{N@QPy~nkSCn4!Ir-bWm#!E`?|%rF8@{<$TcdK7GN`qvVFx zJ`c0v0B4+OqjW*u^HuD8q1;RIyFs}q=8O>c@sI$)vzk#7Lo#1hrue^=dCW~2qkB(2 zSdL5UTVA&cXgG&=ZLRxQG}v{4ft!0UHke4_GrJp9YFAqhx%KYC;W#7fS@;#M5yP`B zRBENKzlgkZJ#L{jlUpz&RM*cmMPL6cOv#auBl@80DCD7%X5=kFwQKSX0<3;c;u9g6 z{o)^2j1-X{*wsO{E;d_g0`4e=22o72X>m9fNg3Zj5!>j$>IcaG;0H!Jc+az7iUa=g zl>gN;4AqtXE62#`lywn-v0cSy9L`CO1|QYGZ-oecu}eC2d3t1$^+*XUnpu45bJ%NT z^eIbBQ6Q?JN>he2N{Vw2eVJ3;>cy&F7Zt}b#8($*wG%24Fu!jvZ>@XDWS5vj5{t%Q z#FT^KOcfSS8wO|Gu)k*uA2#`9p1a-uMjx;pHwbB zc|TdTFQ6l7{B>hwy?Kp}2cEWQ!S>YSTZ!qfFQN_Y@hVLria4^0XZJgJ6ucK()C9k7&$y*xzmzF6I7WYb=veKGfN}f z3@Uy)@!7#xRN!e9x~u6KQd}Kjr}sA|K8_Qte-dKDaQfDbgroUaGeA-GL7b_KI;Xh^ zP2b+`84%Yo8_6d^3KAG;ztK`TQmM}&nR7NiuUv}Utl`p<9sIq3n<^sGkv6{mR z&LF3|?3|+gHk`CVHO^t6$oh}g{(T?m->w>+$p|!QRk}@BixLf#-E%AeTDQm*GUxCN$VoZZPZ>rc1boUtCxGXx58}o&KyoEoBX3x0Il#lSpAV-5BuMa3hh1OjOr7vsl|U7zmZW;?^w|L6@B-uHJWFxPbJoX zFC&SBO?g^WRvS~b>Qaz6%DyPsad}Gj(n_I;Gi$@(qQ6Xv?sBhVOwoM}aIi8^ifsi>L_$RcTxEqn=o^T&kL~@AFaq zpFM79v95Gi9{k*<7N>Bp-;I^+fS$dk3gb6DBlo3_WhAL+h1W(9POSJO(YsRE>w3s5Q)T_`l+o( zym%-rOzCz7*|2o<+3}JtO#~Uoq`Sj^bET#d_4gczCU3@?sy02GzcE z8e~ToXXoJq`xS~f3||IB*fU%?<@EVkq%vJdzQodDm+r&oL?b03Qf?*?Z|m4Ob)2nH zb11UpaMHY17sAFZyOBBGrrJmED5z)o8C%PmxSjCv0GThgb!XV5Mh3hpYf@ij9GCc6 zn3jH1@XT1#oIm`F;W)dd>9zB6X3pU<&yhWQhcaIePo%x9BGrbvrb3M4qwztC&YSDUnQ@!-)(-^GuUk>UDanBBH198^!Q%+ zJyzKSn=zFrxy#IAKT!J@`?w7mDlM+Z886POTvU+21+n03!|TSa%{U?(pWcC52)LP; z53d`MSM3s5wX7>*d_orukM+27;gF*mA(@3RCKlD zI(A5wy=uCPDFOD`tMm$)hW!51!*5>{LxQckVU2?|a(;FSPxvxDN9Y;N7{4>?h&qp< zoFxx)oty1&L8LG}0T0CR7_-u}vONT~gfgfI^%UM>%W$)yb}viT@pJwfKFx+l&}X{| z2IZ+b1<`$y&?~8V7k)_4XJ4{JuSk#>5dmtv(QBbCq!QL^mXY&dmm^1H07p%nqYrCm zRyJ72*6i6ZmHIKJvuaE*)_9>H)ypfA4z_1>6b2&MQneXXCw(L1o&@fKAIlwWwwtJr zb`+B8P4jORkrF+DMB}R8r>}HfmQ#)J?Y*OP`^El}1)8cF4){@} zNa`iEPBvSjhD(_7!vd2@rUUylVfSN&L(g3+VG78eg%YzWw6aGDTKXCw7WtvFqu;ay zq_{r|l)x4^^4kk*gyv7+Y_gyWU#X87j)OH>G5kWdUr%0q@I~3v_t4?so5@JeJ2(AQ zmOd08!E#J%u^iUhdoI0NUVcK$Jn-7?VYrp}fg3j!_u|zKLO8K|2(K>`J&lft zVbn-dJPwf`q{?PEbtGDN*{OD@p9QwSMpPYw7~iSqW`$d6g8~-fL$z$p5e8libm2$H z6VW{tqQTKARDn<4-g3WAjwYoG4YyzPoPHE2rIA-9?uYV)^mV9c zv3h4$S9DU*hMq)^OcRqIR!ayAosxC>ypO6*=ydB7Ckra2Bj(?aJ6^(m-yN1({wepo zL~yNrfxn0+k#J8?XE~_O43fgPXDF5{eO6zkV&E#*nX?x4&I2u(k7`c=0Xv_3^1}#S zdEfpA!F~G;(Ka}_Pvw$Jh8U2TcAvw=88uI9S0q*CC>xI<`a%$i-1oxHxsRo)Bh&yq zt{wl=5!>|UVfLX=OEyhbG`?tWXN};Qg~MPnD}i?A3cd#D3xQPbMW&R|UHYF+CT>q-olmyL0g)LfcryCN2A zqY^tG9YKEbb#UP>E9uhLp>fLtJTaQ?$)oP`{p-Biw|}(eWv!b}6UWxQ3>52dCtrNJ zxO}5j-i=TFPOF|`$*VM#g}L{ZmllWbT#`l-r%Q4crrDt>`yE4W)VfEB^mcS~=qaww zJ`ENC$*;62u4}uSVa&u+Q1t8B_f1iQCK6v<&tF=ZY4}#PzE<|FvLnj1YW22Krq;=7 z=T_>6?^u;zpVC!Lo>w7jv(i2 z)ZJle7Fp^Yu@f9{?{;r%ZT`;Aj_yp8Y|GEdJ60Jc2EP@2JDiqP?0aWRAc7@~pX=l0 z5LC(O*_6rW2tgX7AcF{UQP3%T*%yYlC#pEa+2fP^-t_l=3(AS_?|c@Bsb4fGHnXAW zFY@UooxFSIr$CX8FEWu zeXxD>K|UWGBnIb5K>7;qA!Nyy2ps<6b=sNcXdB)Au&nd0lUCtR1dPuJ{}8^+a4|F4 z0BY4v9|8v>NILe0q|}hXRgok0r5Om>!lqzfhEQ@l-j#O`jcbFquG^Zq zw(+;Hmx|wFS}s1R;?H{IAZ|R9s!5p7(j?V=O(i}L{1F*94!Zb+E8E(wTyTiFFHTjN zSrY%~c<_lrt7@hT2t5*0q7|)9?f#D+zRF(wXz8MVApV>Nb@IG@0_JIuRKB#tT-I@C zH_hy*F;)Sl_<=Wdv)ua=@rI9Etjs8 zpn%LU|2LKxejyH?sr>UTxwN;A8V5Zlx7&A14@+=EgGQ``<;75iyFp)z{YYmF53M4| zjDsd>gx06hcbYX?EO;h(95bRi^#JMfFbz>K5pAgwT~c;UR)E1oX{5|-xs(Ep2;W)c z2hvZmnI&u5TTCb;s8w{gcV_ae=*Xkac8-rrj4sl9ilvXQ5=gy#QDsN+PdD3Uw=m8h zrY@t}x5z;V!OTLtbf$Iqk3Y~K;`6?C^wV|EioB?~DJ$g2OdE)%TJ$;$)}p?z!>0O+ zxu0W(6~|F!yiZ-_^eTZ+pECJmP0>Uc^XXkCIRAA-qoNwi9mo>7V6_7`60>Tk5DdS6 zP(JWn=Xnypx7fi4u2wG3%zYM1)|cQJ2u{&C5=fw z$7h5~yw>^&kN(?===MmSmRI zJ=9u2)+)B3H$;b6?kL%&h~}?M@9J%Lcs*s&uSp zKaQkAotv#r@0qeo%Zb(HCSfC}vPm6`QAvK)d!l*)_Y-&>)vQXVV3ZgS6P1ifE#r@_azu|p+6>-J4o|JAhyCV#jM(aozHdR^wY#U25oS@;Tj#Auh`QI@666hiR^fhcSBR zW>28^q$kEcgsnA?tUPNjE7Jnmd-FFJ5L z5)GIUh%`}Rj9&)<7-%+Y?qe<7bGn}lVRUvtxN;O-!E z&2}($;AmGOllkRI4POo7)RKkMub zIS9{|`B%wbK(su(D+l5aa2t1X(YB(E{q4&F{1H;uWcis=J~O&d(YV?e>Bqx-&6xsj zL(=mubbjPaurKs0Cw*Z_vx?ziA1k%6?zgNlhPuhg^{cs=?!k-*e2fSZfXYV6N0rcN z%S|GXzEFKyZt`#DTKneOQ^hD5 zpLBRvb7j*-6S%%2=hd}!uXX2DQ{^;Q+4XE_@8e7Cnoxpb# z+bW2bypAB+=+NsbQ%O?k4SptF=0S#P?enX)V)DJ_G|c1bL}!M7CA)vrF93FFZ?;r_ z<)92rRryD5Ug^d7Wc30gdtR|XP1Tc^wS?zqyScot`#UYqg~E2n_kdh9TdX09oh@g5 zP$j&eo`hr=cS@?&=g%>ksu}n@+13vCzB$|p^~@P?rm=6fZIIWuaG6@L(BsUos=mx1 z(5#OLs9SJ*D`Vzt^yAet#)2l_;_MrC&4eT$ka}+<0w^l|{=pJ(8fv@tJ=9_gU8`0m z@ii(LJ?Bo_TyU?PEU#s{U}kBrH#z(+^07+Pqpa_p49V8bk-9eG@bO40yj#o}6u*=Q z-8X;N7y@lo&ZSG@B^k%XNLis)R$dhwYTkGK!gt9sDdB@6Lf!?R-ZkC00(zTm+Gm$7 znE$0jC8a(+o?K3TF44L?8NMza&)ZxYxs)d$!zpv4JTbeUW3Os*AQkn?C1^Z31b!cH ze<$kwRZo@?B8|P$YGC-j2geju#k_C17eql-?Z9w(#h(yMeTEa>W#1O^)*$!C4jra` zU40Gri0FPqNVmamwJ$~m`rh*X*04DFs7`}B6oibS&~D54zsB(~7{ zi^t%-rwXVO^lB~W@R%BRZ--S)NLlh{qb^H_7o??yD^t-9OACi$&gn~X-(qxm@9QqU zV###RCS1Rue1C&{uYXxp<=7z=niL@VBh`%|<4zR<(8PY{Jki+qrgJm19ZD_2Miy-D zS7Z5iPiUivB`xZabKU)lILT%6W3A~WQjuj0=V9|JC^p5R>x3NYt8ZE5$1RystC@g| zQ?vTI(O)ohWZJo9|8t7-Hv}0B#%pSKpmr z(BEE-wst|`$Uk;Lk(jN&|H3ZlnYb{&2T%^R!)9FudpM6BNh^8FS$Oi;WM#{8RohCK zZe~)@UIlDvskw`V#l`4aLHk#OOSl!*GG>Ve*cuCy+~pbSG2iAHIca*~)qy>}$%ErZ zxYv(1^Qp@l2&|2%N_L@YoD>yJlnrukv_zstt=|s zR{Kzm%R)aY(Y=iNMXyU{hYRY!(^N}$Q1hjbQQt6!lX-k9?wN{wda>hP=%khm zO}GH_iyXCz2}8@HzFj!pkIxSms@X#mq;3<7@J|Z7=3sgu6$^zEBi;!ip{+9yo(!K+ zbYzWHebM(dDzjtJUm+=SdCI=%UepEm-U1zCj!LfzeTKo}#tTcOipkQ&J-`Q!Gf(ey z;EPY|h9;5ENg40Oc4?>?GW9%eShb09W_|W;{u35D;Q!J2WP!o_oo^#4jicBTuaC~! zxyo5ON52~Os~9M;Xw6(jn`wtmWlflX@Qvj|zU;vgLi1}w0SN&v^P#mfzF0n=vMHJ1 z-m{&ric;k)IS0ZVyRrz6G1?>HQO1e61xK_xhQ9~Md_9+#I%+DzK68fXWuuJjfGp!h zzE(1;r8OQT!IC3Ff-K5--b(8B=}%{9@XJV*KS`j;G|YLMX4z80S!_CWlCR;$7>cPp zmicagao6-~8es$P$NFAPoKGj{3NNqiu9~=|H3moWIGJ+Mp4HSmiA1`RGIYjEB8`rG z@VSCMC}XIk{$R+B3KGp$6msP8m@mJ4Te~-)wdc#{c=~y1YAOjm4CGxwDk@oChydrv zL(^3GyLbN-s;1vJtlXdSo!V!e%A31nHA!)yTi^2^;&LNWHUkq4>>m93RtNik`e+OL zhp)EVfw{S>K}iXJ?rQvpQ2lRrHF7$&bopQ$!FVl(gGYTqSFggej;U-__Sd8W1T zkl^B@P95s7@2ruLXDV4gJ^K1?EwH#4!~0pn`Zz1DXr1*SC$pU2H)rLP=L0=h;C=dw zlw$p&(#Yqzxwjq9awnUb7*+2!#kZS!3TTg2XpvPE>tWX%MbO#rPScn5MnamtU#JdZ zkh^V54C2c*=>|I=5Lfw>E--gRTwYF5u_wl8Ei?1b+Q7h-zE7V%_4W1j_8Mr_buqB? z&&(Kb$i*J)wz;h+4!RpUd#oqo0fcclH#Zj|Eq#9JY;_Rn9d_5KtBcE?4_BNZ>4z74 zj?B9t(DHA5bZaWahfeHXSzKJ~?&dLC@~p0f^YRsG_s`9Fi`nTf<)!WK91b`2Q(^Al z{4Vn*g4RjM8Bzv~BIG{SP=X%Sk#WFd3F6Jr5Z~OpErIzb}kxoJKmld?Wi^$vgK_lM)(7 zGRk7n;`N0l!>x8p*B0@}cJce8pU+peR$*J>nM7X6S`VdqxMU}t$n>dJXBXk~&nLCL zvN4xynZ+dtPo1B*>u&fX(LKf{ni>8hT`&%D0*u-(naNr`bbbNaIaN^{mRtSghZ&r2 zRCVTUl+&C2Ad&*J8LL(UnCB072xk8!^AJ-tBVMJtSG$@NKHPWid|*jx6xRA+ z?<%Lm`69h#l1?afRS{{_8A}ys+AL7m=pU>&@6@FIPibf8hyWIg*-1Npo)OqOC9-jD z#01zU+Q|$X=SH@Eq)iZD${y@i()e=-2Hr?ze;rd$pq%d5&UQOrXen*T)&UdP#{TMW zr$DyOjsQ6atZW4O?H|Dtdnd z;M;V5&Hg7czs6u#2m*nE!H{4yg$fpirf>t>9{$^nC&2}@O%MygVsR7+oIwbH4g!tB z0pe|8PCFRDAr6YfqQJjlPzVYE$02ZFI0TD8!;rWwWx=3W0D{BeNMLMJe-wxSFaPg;S^i-m&RJf4`yF00Dzwkx)1m zP!9sf6Ejv9k)`X$U}8U`gmU zJAhVpur0O`z?reXYm)GrFKh?HKe)p77$q11kpZ6J5EvQ-Xk^odI4lqrlu*YYurL%w z5ICR~Krs{>!Z3iJBQb#Y!f_BZ4uyrn02=~|z~RtXBnFItLU3>d8Vx8E&>j>pAo6z% z4#8lslz`ZoP4R0S9E!yO&V&KP$0D(CK(~Np5KuIZLI@2wDijUFP^|R}Ljo8MyOFnk zO$K}t2Vgi#+-R0u}`5Y_szlw$1i$v$q8Vc-&rSM;9CMEV&iX+lKm2M%+n;aEd+QNF?n45Q!9x zBhi#d01^%gjRq!Bq8AB=LylAcCkz5a z6c$j56yVu#_(psJnG=G-Vt`ciJGx!dq+mcE!(f0A0{@0>SQZ7{RL)lQlcHz`MagJ8 z1UD-p1__wx*UH-vMG+Vz8VANCcwzAMR(*^ z#(UwNT7q7YGQbze6ZM zVSkVZO8sA$0Qx-K-yk5J{y7ood4Zj9z|OH&0fySS&Tv#kVJ woMnls_Loz50@V%vH$=8 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/Contents.json deleted file mode 100644 index ed0b87dda..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ra_other_news.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/ra_other_news.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_other_news.imageset/ra_other_news.pdf deleted file mode 100644 index e550a9c046d3e83f5c7defefc34923956f9c48f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12005 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 IWmV<=4>neGP5=M^ diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/Contents.json deleted file mode 100644 index b7c7a6180..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ra_planned_event.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/ra_planned_event.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_planned_event.imageset/ra_planned_event.pdf deleted file mode 100644 index 7cbbef29db4bfcce8efce6e0f8d1f206cb300082..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13986 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 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/Contents.json deleted file mode 100644 index f0a98bc6d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ra_road_closure.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/ra_road_closure.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_road_closure.imageset/ra_road_closure.pdf deleted file mode 100644 index 3d99eab81f006fcc3e0e10a0adc018cfbf4c772d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11476 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 IWmV<=4>neGP5=M^ diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/Contents.json deleted file mode 100644 index 4b7b0bc93..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ra_weather.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/ra_weather.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/RouteAlerts/ra_weather.imageset/ra_weather.pdf deleted file mode 100644 index e809855a942941a59868bb5aedd05403156ac4c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17306 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 b9EgvYKVM<%;l+3jDpo+VMvRy|Ys&utwXjg? diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/Contents.json deleted file mode 100644 index f538e9791..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "midpoint_marker.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/midpoint_marker.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/midpoint_marker.imageset/midpoint_marker.pdf deleted file mode 100644 index 111589808d39a82cb08666252bfeabeaeb3690ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21017 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 F-2h#^9c};s diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/Contents.json b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/Contents.json deleted file mode 100644 index b46e28fbf..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "puck.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/puck.pdf b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Assets.xcassets/puck.imageset/puck.pdf deleted file mode 100644 index e137c6f1b2c6dae2028bd745800a64732e99e37e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19977 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 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/Sounds/reroute-sound.pcm b/ios/Classes/Navigation/MapboxNavigationCore/Resources/Sounds/reroute-sound.pcm deleted file mode 100644 index c089f4d75bd4dce86fb6e829b5346ec786e6b5c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66284 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݂@YLHE#( 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 diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ar.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ar.lproj/Localizable.strings deleted file mode 100644 index bfb547651..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ar.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "وقت الوصول المقدر المماثل"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/bg.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/bg.lproj/Localizable.strings deleted file mode 100644 index d2782f0a6..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/bg.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Подобно време на пристигане"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ca.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ca.lproj/Localizable.strings deleted file mode 100644 index 08d80612a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ca.lproj/Localizable.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* This route does not have tolls */ -"ROUTE_HAS_NO_TOLLS" = "Sense peatges"; - -/* This route does have tolls */ -"ROUTE_HAS_TOLLS" = "Peatges"; - -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Mateixa durada"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/cs.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/cs.lproj/Localizable.strings deleted file mode 100644 index 56a1b8f84..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/cs.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Podobný čas příjezdu"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/da.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/da.lproj/Localizable.strings deleted file mode 100644 index 7c5c42e7f..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/da.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Lignende ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/de.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/de.lproj/Localizable.strings deleted file mode 100644 index 7245b1e65..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/de.lproj/Localizable.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* This route does not have tolls */ -"ROUTE_HAS_NO_TOLLS" = "Keine Mautgebühr"; - -/* This route does have tolls */ -"ROUTE_HAS_TOLLS" = "Mautgebühren"; - -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Ähnliche ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/el.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/el.lproj/Localizable.strings deleted file mode 100644 index 681a66554..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/el.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Παρόμοια ώρα άφιξης"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/en.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/en.lproj/Localizable.strings deleted file mode 100644 index ad702bf05..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/en.lproj/Localizable.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* This route does not have tolls */ -"ROUTE_HAS_NO_TOLLS" = "No Tolls"; - -/* This route does have tolls */ -"ROUTE_HAS_TOLLS" = "Tolls"; - -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Similar ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/es.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/es.lproj/Localizable.strings deleted file mode 100644 index 758bd1e58..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/es.lproj/Localizable.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* This route does not have tolls */ -"ROUTE_HAS_NO_TOLLS" = "Sin peaje"; - -/* This route does have tolls */ -"ROUTE_HAS_TOLLS" = "Peaje"; - -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "ETA similar"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/et.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/et.lproj/Localizable.strings deleted file mode 100644 index 3cd52962b..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/et.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Sarnane ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/fi.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/fi.lproj/Localizable.strings deleted file mode 100644 index b31de362c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/fi.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Samanlainen arvioitu saapumisaika"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/fr.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/fr.lproj/Localizable.strings deleted file mode 100644 index 83a27df2d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/fr.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "ETA similaire"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/he.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/he.lproj/Localizable.strings deleted file mode 100644 index b4bcc5db1..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/he.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "זמן הגעה משוער דומה"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/hr.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/hr.lproj/Localizable.strings deleted file mode 100644 index fe645876b..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/hr.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Sličan ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/hu.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/hu.lproj/Localizable.strings deleted file mode 100644 index 51bcc1fcf..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/hu.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Hasonló érkezési idő"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/it.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/it.lproj/Localizable.strings deleted file mode 100644 index 7a365d371..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/it.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "ETA simile"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ja.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ja.lproj/Localizable.strings deleted file mode 100644 index 96e71f337..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ja.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "似たようなETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ms.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ms.lproj/Localizable.strings deleted file mode 100644 index 9552061ff..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ms.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "ETA Serupa"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/nl.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/nl.lproj/Localizable.strings deleted file mode 100644 index 2fa42fb80..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/nl.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Vergelijkbare ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/no.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/no.lproj/Localizable.strings deleted file mode 100644 index 7c5c42e7f..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/no.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Lignende ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/pl.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/pl.lproj/Localizable.strings deleted file mode 100644 index c15c13b2e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/pl.lproj/Localizable.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* This route does not have tolls */ -"ROUTE_HAS_NO_TOLLS" = "Bez opłat"; - -/* This route does have tolls */ -"ROUTE_HAS_TOLLS" = "Opłaty"; - -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Podobny czas przybycia"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-BR.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-BR.lproj/Localizable.strings deleted file mode 100644 index 47f1dc2c6..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-BR.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "ETA semelhante"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-PT.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-PT.lproj/Localizable.strings deleted file mode 100644 index 47f1dc2c6..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/pt-PT.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "ETA semelhante"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ro.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ro.lproj/Localizable.strings deleted file mode 100644 index 86fc33b52..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ro.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "ETA similar"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ru.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/ru.lproj/Localizable.strings deleted file mode 100644 index 931874105..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/ru.lproj/Localizable.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* This route does not have tolls */ -"ROUTE_HAS_NO_TOLLS" = "Нет оплаты"; - -/* This route does have tolls */ -"ROUTE_HAS_TOLLS" = "Платная дорога"; - -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "То же время"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sk.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sk.lproj/Localizable.strings deleted file mode 100644 index fe645876b..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sk.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Sličan ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sl.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sl.lproj/Localizable.strings deleted file mode 100644 index 65b2b1f64..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sl.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Podoben ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sr.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sr.lproj/Localizable.strings deleted file mode 100644 index 273ded63a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sr.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Podobný ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sv.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/sv.lproj/Localizable.strings deleted file mode 100644 index c0d84e5b4..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/sv.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Liknande ankomsttid"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/tr.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/tr.lproj/Localizable.strings deleted file mode 100644 index ea0b15aa1..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/tr.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Benzer ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/uk.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/uk.lproj/Localizable.strings deleted file mode 100644 index d18126f0e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/uk.lproj/Localizable.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* This route does not have tolls */ -"ROUTE_HAS_NO_TOLLS" = "Проїзд бесплатний"; - -/* This route does have tolls */ -"ROUTE_HAS_TOLLS" = "Проїзд платний"; - -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Схожий ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/vi.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/vi.lproj/Localizable.strings deleted file mode 100644 index 750b1fcc4..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/vi.lproj/Localizable.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* This route does not have tolls */ -"ROUTE_HAS_NO_TOLLS" = "Miễn phí"; - -/* This route does have tolls */ -"ROUTE_HAS_TOLLS" = "Thu phí"; - -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "Cùng Thời gian"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hans.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index e6fada04a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "類似 ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hant.lproj/Localizable.strings b/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index e6fada04a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Resources/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Alternatives selection note about equal travel time. */ -"SAME_TIME" = "類似 ETA"; diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Routing/MapboxRoutingProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Routing/MapboxRoutingProvider.swift deleted file mode 100644 index 7313e9398..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Routing/MapboxRoutingProvider.swift +++ /dev/null @@ -1,253 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxCommon_Private -import MapboxDirections -import MapboxNavigationNative -import MapboxNavigationNative_Private - -/// RouterInterface from MapboxNavigationNative. -typealias RouterInterfaceNative = MapboxNavigationNative_Private.RouterInterface - -struct RoutingProviderConfiguration: Sendable { - var source: RoutingProviderSource - var nativeHandlersFactory: NativeHandlersFactory - var credentials: Credentials -} - -/// Provides alternative access to routing API. -/// -/// Use this class instead `Directions` requests wrapper to request new routes or refresh an existing one. Depending on -/// ``RoutingProviderSource``, ``MapboxRoutingProvider`` will use online and/or onboard routing engines. This may be -/// used when designing purely online or offline apps, or when you need to provide best possible service regardless of -/// internet collection. -public final class MapboxRoutingProvider: RoutingProvider, @unchecked Sendable { - /// Initializes a new ``MapboxRoutingProvider``. - init(with configuration: RoutingProviderConfiguration) { - self.configuration = configuration - } - - // MARK: Configuration - - let configuration: RoutingProviderConfiguration - - // MARK: Performing and Parsing Requests - - private lazy var router: RouterInterfaceNative = { - let factory = configuration.nativeHandlersFactory - return RouterFactory.build( - for: configuration.source.nativeSource, - cache: factory.cacheHandle, - config: factory.configHandle(), - historyRecorder: factory.historyRecorderHandle - ) - }() - - struct ResponseDisposition: Decodable { - var code: String? - var message: String? - var error: String? - - private enum CodingKeys: CodingKey { - case code, message, error - } - } - - // MARK: Routes Calculation - - /// Begins asynchronously calculating routes using the given options and delivers the results to a closure. - /// - /// Depending on configured ``RoutingProviderSource``, this method may retrieve the routes asynchronously from the - /// [Mapbox Directions API](https://www.mapbox.com/api-documentation/navigation/#directions) over a network - /// connection or use onboard routing engine with available offline data. - /// Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). - /// - Parameter options: A `RouteOptions` object specifying the requirements for the resulting routes. - /// - Returns: Related request task. If, while waiting for the completion handler to execute, you no longer want the - /// resulting routes, cancel corresponding task using this handle. - public func calculateRoutes(options: RouteOptions) -> FetchTask { - return Task { [ - sendableSelf = UncheckedSendable(self), - sendableOptions = UncheckedSendable(options) - ] in - var result: Result - var origin: RouterOrigin - (result, origin) = await sendableSelf.value.doRequest(options: sendableOptions.value) - - switch result { - case .success(let routeResponse): - guard let navigationRoutes = try? await NavigationRoutes( - routeResponse: routeResponse, - routeIndex: 0, - responseOrigin: origin - ) else { - throw DirectionsError.unableToRoute - } - return navigationRoutes - case .failure(let error): - throw error - } - } - } - - /// Begins asynchronously calculating matches using the given options and delivers the results to a closure. - /// - /// Depending on configured ``RoutingProviderSource``, this method may retrieve the matches asynchronously from the - /// [Mapbox Map Matching API](https://docs.mapbox.com/api/navigation/#map-matching) over a network connection or use - /// onboard routing engine with available offline data. - /// - Parameter options: A `MatchOptions` object specifying the requirements for the resulting matches. - /// - Returns: Related request task. If, while waiting for the completion handler to execute, you no longer want the - /// resulting routes, cancel corresponding task using this handle. - public func calculateRoutes(options: MatchOptions) -> FetchTask { - return Task { [ - sendableSelf = UncheckedSendable(self), - sendableOptions = UncheckedSendable(options) - ] in - var result: Result - var origin: RouterOrigin - (result, origin) = await sendableSelf.value.doRequest(options: sendableOptions.value) - - switch result { - case .success(let routeResponse): - guard let navigationRoutes = try? await NavigationRoutes( - routeResponse: RouteResponse( - matching: routeResponse, - options: options, - credentials: .init(sendableSelf.value.configuration.nativeHandlersFactory.apiConfiguration) - ), - routeIndex: 0, - responseOrigin: origin - ) else { - throw DirectionsError.unableToRoute - } - return navigationRoutes - case .failure(let error): - throw error - } - } - } - - private func doRequest(options: DirectionsOptions) async -> (Result< - ResponseType, - DirectionsError - >, RouterOrigin) { - let directionsUri = Directions.url(forCalculating: options, credentials: configuration.credentials) - .removingSKU().absoluteString - let (result, origin) = await withCheckedContinuation { continuation in - let routeSignature = GetRouteSignature(reason: .newRoute, origin: .platformSDK, comment: "") - router.getRouteForDirectionsUri( - directionsUri, - options: GetRouteOptions(timeoutSeconds: nil), - caller: routeSignature - ) { ( - result: Expected, - origin: RouterOrigin - ) in - continuation.resume(returning: (result, origin)) - } - } - - return await ( - parseResponse( - userInfo: [ - .options: options, - .credentials: Credentials(configuration.nativeHandlersFactory.apiConfiguration), - ], - result: result - ), - origin - ) - } - - private func parseResponse( - userInfo: [CodingUserInfoKey: Any], - result: Expected - ) async -> Result { - guard let dataRef = result.value else { - return .failure(.noData) - } - - return parseResponse( - userInfo: userInfo, - result: dataRef.data, - error: result.error as? Error - ) - } - - private func parseResponse( - userInfo: [CodingUserInfoKey: Any], - result: Expected - ) async -> Result { - let json = result.value as String? - guard let data = json?.data(using: .utf8) else { - return .failure(.noData) - } - - return parseResponse( - userInfo: userInfo, - result: data, - error: result.error as? Error - ) - } - - private func parseResponse( - userInfo: [CodingUserInfoKey: Any], - result data: Data, - error: Error? - ) -> Result { - do { - let decoder = JSONDecoder() - decoder.userInfo = userInfo - - guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { - let apiError = DirectionsError( - code: nil, - message: nil, - response: nil, - underlyingError: error - ) - return .failure(apiError) - } - - guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { - let apiError = DirectionsError( - code: disposition.code, - message: disposition.message, - response: nil, - underlyingError: error - ) - return .failure(apiError) - } - - let result = try decoder.decode(ResponseType.self, from: data) - return .success(result) - } catch { - let bailError = DirectionsError(code: nil, message: nil, response: nil, underlyingError: error) - return .failure(bailError) - } - } -} - -extension ProfileIdentifier { - var nativeProfile: RoutingProfile { - let mode: RoutingMode = switch self { - case .automobile: - .driving - case .automobileAvoidingTraffic: - .drivingTraffic - case .cycling: - .cycling - case .walking: - .walking - default: - .driving - } - return RoutingProfile(mode: mode, account: "mapbox") - } -} - -extension URL { - func removingSKU() -> URL { - var urlComponents = URLComponents(string: absoluteString)! - let filteredItems = urlComponents.queryItems?.filter { $0.name != "sku" } - urlComponents.queryItems = filteredItems - return urlComponents.url! - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Routing/NavigationRouteOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/Routing/NavigationRouteOptions.swift deleted file mode 100644 index 7ceed37e2..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Routing/NavigationRouteOptions.swift +++ /dev/null @@ -1,227 +0,0 @@ -import _MapboxNavigationHelpers -import CoreLocation -import Foundation -import MapboxDirections - -/// A ``NavigationRouteOptions`` object specifies turn-by-turn-optimized criteria for results returned by the Mapbox -/// Directions API. -/// -/// ``NavigationRouteOptions`` is a subclass of `RouteOptions` that has been optimized for navigation. Pass an instance -/// of this class into the ``RoutingProvider/calculateRoutes(options:)-3d0sf`` method. -/// -/// This class implements the `NSCopying` protocol by round-tripping the object through `JSONEncoder` and `JSONDecoder`. -/// If you subclass ``NavigationRouteOptions``, make sure any properties you add are accounted for in `Decodable(from:)` -/// and `Encodable.encode(to:)`. If your subclass contains any customizations that cannot be represented in JSON, make -/// sure the subclass overrides `NSCopying.copy(with:)` to persist those customizations. -/// -/// ``NavigationRouteOptions`` is designed to be used with the ``MapboxRoutingProvider`` class for specifying routing -/// criteria. -open class NavigationRouteOptions: RouteOptions, OptimizedForNavigation, @unchecked Sendable { - /// Specifies the preferred distance measurement unit. - /// - /// Meters and feet will be used when the presented distances are small enough. See `DistanceFormatter` for more - /// information. - public var distanceUnit: LengthFormatter.Unit = Locale.current.usesMetricSystem ? .kilometer : .mile - - public convenience init( - waypoints: [Waypoint], - profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, - queryItems: [URLQueryItem]? = nil, - locale: Locale, - distanceUnit: LengthFormatter.Unit - ) { - self.init( - waypoints: waypoints, - profileIdentifier: profileIdentifier, - queryItems: queryItems - ) - self.locale = locale - self.distanceUnit = distanceUnit - } - - /// Initializes a navigation route options object for routes between the given waypoints and an optional profile - /// identifier optimized for navigation. - public required init( - waypoints: [Waypoint], - profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, - queryItems: [URLQueryItem]? = nil - ) { - super.init( - waypoints: waypoints.map { waypoint in - with(waypoint) { - $0.coordinateAccuracy = -1 - } - }, - profileIdentifier: profileIdentifier, - queryItems: queryItems - ) - includesAlternativeRoutes = true - attributeOptions = [.expectedTravelTime, .maximumSpeedLimit] - if profileIdentifier == .automobileAvoidingTraffic { - attributeOptions.update(with: .numericCongestionLevel) - } - includesExitRoundaboutManeuver = true - if profileIdentifier == .automobileAvoidingTraffic { - refreshingEnabled = true - } - - optimizeForNavigation() - } - - /// Initializes an equivalent `RouteOptions` object from a ``NavigationMatchOptions``. - /// - /// - SeeAlso: ``NavigationMatchOptions``. - public convenience init(navigationMatchOptions options: NavigationMatchOptions) { - self.init(waypoints: options.waypoints, profileIdentifier: options.profileIdentifier) - } - - /// Initializes a navigation route options object for routes between the given locations and an optional profile - /// identifier optimized for navigation. - public convenience init( - locations: [CLLocation], - profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, - queryItems: [URLQueryItem]? = nil - ) { - self.init( - waypoints: locations.map { Waypoint(location: $0) }, - profileIdentifier: profileIdentifier, - queryItems: queryItems - ) - } - - /// Initializes a route options object for routes between the given geographic coordinates and an optional profile - /// identifier optimized for navigation. - public convenience init( - coordinates: [CLLocationCoordinate2D], - profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, - queryItems: [URLQueryItem]? = nil - ) { - self.init( - waypoints: coordinates.map { Waypoint(coordinate: $0) }, - profileIdentifier: profileIdentifier, - queryItems: queryItems - ) - } - - public required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } -} - -/// A ``NavigationMatchOptions`` object specifies turn-by-turn-optimized criteria for results returned by the Mapbox Map -/// Matching API. -/// -/// `NavigationMatchOptions`` is a subclass of `MatchOptions` that has been optimized for navigation. Pass an instance -/// of this class into the `Directions.calculateRoutes(matching:completionHandler:).` method. -/// -/// - Note: it is very important you specify the `waypoints` for the route. Usually the only two values for this -/// `IndexSet` will be 0 and the length of the coordinates. Otherwise, all coordinates passed through will be considered -/// waypoints. -open class NavigationMatchOptions: MatchOptions, OptimizedForNavigation, @unchecked Sendable { - /// Specifies the preferred distance measurement unit. - /// - /// Meters and feet will be used when the presented distances are small enough. See `DistanceFormatter` for more - /// information. - public var distanceUnit: LengthFormatter.Unit = Locale.current.usesMetricSystem ? .kilometer : .mile - - public convenience init( - waypoints: [Waypoint], - profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, - queryItems: [URLQueryItem]? = nil, - distanceUnit: LengthFormatter.Unit - ) { - self.init( - waypoints: waypoints, - profileIdentifier: profileIdentifier, - queryItems: queryItems - ) - self.distanceUnit = distanceUnit - } - - /// Initializes a navigation route options object for routes between the given waypoints and an optional profile - /// identifier optimized for navigation. - /// - /// - Seealso: `RouteOptions`. - public required init( - waypoints: [Waypoint], - profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, - queryItems: [URLQueryItem]? = nil - ) { - super.init( - waypoints: waypoints.map { waypoint in - with(waypoint) { - $0.coordinateAccuracy = -1 - } - }, - profileIdentifier: profileIdentifier, - queryItems: queryItems - ) - attributeOptions = [.expectedTravelTime] - if profileIdentifier == .automobileAvoidingTraffic { - attributeOptions.update(with: .numericCongestionLevel) - } - if profileIdentifier == .automobile || profileIdentifier == .automobileAvoidingTraffic { - attributeOptions.insert(.maximumSpeedLimit) - } - - optimizeForNavigation() - } - - /// Initializes a navigation match options object for routes between the given locations and an optional profile - /// identifier optimized for navigation. - public convenience init( - locations: [CLLocation], - profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, - queryItems: [URLQueryItem]? = nil - ) { - self.init( - waypoints: locations.map { Waypoint(location: $0) }, - profileIdentifier: profileIdentifier, - queryItems: queryItems - ) - } - - /// Initializes a navigation match options object for routes between the given geographic coordinates and an - /// optional profile identifier optimized for navigation. - public convenience init( - coordinates: [CLLocationCoordinate2D], - profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, - queryItems: [URLQueryItem]? = nil - ) { - self.init( - waypoints: coordinates.map { Waypoint(coordinate: $0) }, - profileIdentifier: profileIdentifier, - queryItems: queryItems - ) - } - - public required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } -} - -protocol OptimizedForNavigation: AnyObject { - var includesSteps: Bool { get set } - var routeShapeResolution: RouteShapeResolution { get set } - var shapeFormat: RouteShapeFormat { get set } - var attributeOptions: AttributeOptions { get set } - var locale: Locale { get set } - var distanceMeasurementSystem: MeasurementSystem { get set } - var includesSpokenInstructions: Bool { get set } - var includesVisualInstructions: Bool { get set } - var distanceUnit: LengthFormatter.Unit { get } - - func optimizeForNavigation() -} - -extension OptimizedForNavigation { - func optimizeForNavigation() { - shapeFormat = .polyline6 - includesSteps = true - routeShapeResolution = .full - includesSpokenInstructions = true - locale = Locale.nationalizedCurrent - distanceMeasurementSystem = .init(distanceUnit) - includesVisualInstructions = true - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Routing/RoutingProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Routing/RoutingProvider.swift deleted file mode 100644 index 05914e1d1..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Routing/RoutingProvider.swift +++ /dev/null @@ -1,45 +0,0 @@ -import MapboxDirections -import MapboxNavigationNative - -/// Allows fetching ``NavigationRoutes`` by given parameters. -public protocol RoutingProvider: Sendable { - /// An asynchronous cancellable task for fetching a route. - typealias FetchTask = Task - - /// Creates a route by given `options`. - /// - /// This may be online or offline route, depending on the configuration and network availability. - func calculateRoutes(options: RouteOptions) -> FetchTask - - /// Creates a map matched route by given `options`. - /// - /// This may be online or offline route, depending on the configuration and network availability. - func calculateRoutes(options: MatchOptions) -> FetchTask -} - -/// Defines source of routing engine to be used for requests. -public enum RoutingProviderSource: Equatable, Sendable { - /// Fetch data online only. - /// - /// Such ``MapboxRoutingProvider`` is equivalent of using bare `Directions` wrapper. - case online - /// Use offline data only. - /// - /// In order for such ``MapboxRoutingProvider`` to function properly, proper navigation data should be available - /// offline. `.offline` routing provider will not be able to refresh routes. - case offline - /// Attempts to use ``RoutingProviderSource/online`` with fallback to ``RoutingProviderSource/offline``. - /// `.hybrid` routing provider will be able to refresh routes only using internet connection. - case hybrid - - var nativeSource: RouterType { - switch self { - case .online: - return .online - case .offline: - return .onboard - case .hybrid: - return .hybrid - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/SdkInfo.swift b/ios/Classes/Navigation/MapboxNavigationCore/SdkInfo.swift deleted file mode 100644 index 98b642614..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/SdkInfo.swift +++ /dev/null @@ -1,23 +0,0 @@ -import MapboxCommon_Private - -public struct SdkInfo: Sendable { - public static let navigationUX: Self = .init( - name: Bundle.resolvedNavigationSDKName, - version: Bundle.mapboxNavigationVersion, - packageName: "com.mapbox.navigationUX" - ) - - public static let navigationCore: Self = .init( - name: Bundle.navigationCoreName, - version: Bundle.mapboxNavigationVersion, - packageName: "com.mapbox.navigationCore" - ) - - public let name: String - public let version: String - public let packageName: String - - var native: SdkInformation { - .init(name: name, version: version, packageName: packageName) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/AlternativeRoutesDetectionConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/AlternativeRoutesDetectionConfig.swift deleted file mode 100644 index ba8326079..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/AlternativeRoutesDetectionConfig.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation - -/// Options to configure fetching, detecting, and accepting ``AlternativeRoute``s during navigation. -public struct AlternativeRoutesDetectionConfig: Equatable, Sendable { - public struct AcceptionPolicy: OptionSet, Sendable { - public typealias RawValue = UInt - public var rawValue: UInt - - public init(rawValue: UInt) { - self.rawValue = rawValue - } - - public static let unfiltered = AcceptionPolicy(rawValue: 1 << 0) - public static let fasterRoutes = AcceptionPolicy(rawValue: 1 << 1) - public static let shorterRoutes = AcceptionPolicy(rawValue: 1 << 2) - } - - /// Enables requesting for new alternative routes after passing a fork (intersection) where another alternative - /// route branches. The default value is `true`. - @available(*, deprecated, message: "This feature no longer has any effect.") - public var refreshesAfterPassingDeviation = true - - /// Enables periodic requests when there are no known alternative routes yet. The default value is - /// ``AlternativeRoutesDetectionConfig/RefreshOnEmpty/noPeriodicRefresh``. - @available( - *, - deprecated, - message: "This feature no longer has any effect other then setting the refresh interval. Use 'refreshIntervalSeconds' instead to configure the refresh interval directly." - ) - public var refreshesWhenNoAvailableAlternatives: RefreshOnEmpty = .noPeriodicRefresh { - didSet { - if let refreshIntervalSeconds = refreshesWhenNoAvailableAlternatives.refreshIntervalSeconds { - self.refreshIntervalSeconds = refreshIntervalSeconds - } - } - } - - /// Describes how periodic requests for ``AlternativeRoute``s should be made. - @available(*, deprecated, message: "This feature no longer has any effect.") - public enum RefreshOnEmpty: Equatable, Sendable { - /// Will not do periodic requests for alternatives. - case noPeriodicRefresh - /// Requests will be made with the given time interval. Using this option may result in increased traffic - /// consumption, but help detect alternative routes - /// which may appear during road conditions change during the trip. The default value is `5 minutes`. - case refreshesPeriodically(TimeInterval = AlternativeRoutesDetectionConfig.defaultRefreshIntervalSeconds) - } - - public var acceptionPolicy: AcceptionPolicy - - /// The refresh alternative routes interval. 5 minutes by default. Minimum 30 seconds. - public var refreshIntervalSeconds: TimeInterval - - /// Creates a new alternative routes detection configuration. - /// - /// - Parameters: - /// - refreshesAfterPassingDeviation: Enables requesting for new alternative routes after passing a fork - /// (intersection) where another alternative route branches. - /// The default value is `true`. - /// - refreshesWhenNoAvailableAlternatives: Enables periodic requests when there are no known alternative routes - /// yet. The default value is ``AlternativeRoutesDetectionConfig/RefreshOnEmpty/noPeriodicRefresh``. - /// - acceptionPolicy: The acceptance policy. - @available(*, deprecated, message: "Use 'init(acceptionPolicy:refreshIntervalSeconds:)' instead.") - public init( - refreshesAfterPassingDeviation: Bool = true, - refreshesWhenNoAvailableAlternatives: RefreshOnEmpty = .noPeriodicRefresh, - acceptionPolicy: AcceptionPolicy = .unfiltered - ) { - self.refreshesAfterPassingDeviation = refreshesAfterPassingDeviation - self.refreshesWhenNoAvailableAlternatives = refreshesWhenNoAvailableAlternatives - self.acceptionPolicy = acceptionPolicy - self.refreshIntervalSeconds = refreshesWhenNoAvailableAlternatives.refreshIntervalSeconds ?? Self - .defaultRefreshIntervalSeconds - } - - /// Creates a new alternative routes detection configuration. - /// - /// - Parameters: - /// - acceptionPolicy: The acceptance policy. - /// - refreshIntervalSeconds: The refresh alternative routes interval. 5 minutes by default. Minimum 30 - /// seconds. - public init( - acceptionPolicy: AcceptionPolicy = .unfiltered, - refreshIntervalSeconds: TimeInterval = Self.defaultRefreshIntervalSeconds - ) { - self.acceptionPolicy = acceptionPolicy - self.refreshIntervalSeconds = refreshIntervalSeconds - } - - public static let defaultRefreshIntervalSeconds: TimeInterval = 300 -} - -@available(*, deprecated) -extension AlternativeRoutesDetectionConfig.RefreshOnEmpty { - fileprivate var refreshIntervalSeconds: TimeInterval? { - switch self { - case .noPeriodicRefresh: - return nil - case .refreshesPeriodically(let value): - return value - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/BillingHandlerProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/BillingHandlerProvider.swift deleted file mode 100644 index 4bb51d464..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/BillingHandlerProvider.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -struct BillingHandlerProvider: Equatable { - static func == (lhs: BillingHandlerProvider, rhs: BillingHandlerProvider) -> Bool { - return lhs.object === rhs.object - } - - private var object: BillingHandler - func callAsFunction() -> BillingHandler { - return object - } - - init(_ object: BillingHandler) { - self.object = object - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/CoreConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/CoreConfig.swift deleted file mode 100644 index f81cc3779..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/CoreConfig.swift +++ /dev/null @@ -1,222 +0,0 @@ -import CoreLocation -import Foundation -import MapboxCommon -import MapboxDirections - -/// Mutable Core SDK configuration. -public struct CoreConfig: Equatable { - /// Describes the context under which a manual switching between legs is happening. - public struct MultiLegAdvanceContext: Sendable, Equatable { - /// The leg index of a destination user has arrived to. - public let arrivedLegIndex: Int - } - - /// Allows to manually or automatically switch legs on a multileg route. - public typealias MultilegAdvanceMode = ApprovalModeAsync - - /// SDK Credentials. - public let credentials: NavigationCoreApiConfiguration - - /// Configures route request. - public var routeRequestConfig: RouteRequestConfig - - /// Routing Configuration. - public var routingConfig: RoutingConfig - - /// Custom metadata that can be used with events in the telemetry pipeline. - public let telemetryAppMetadata: TelemetryAppMetadata? - - /// Sources for location and route drive simulation. Defaults to ``LocationSource/live``. - public var locationSource: LocationSource - - /// Logging level for Mapbox SDKs. Defaults to `.warning`. - public var logLevel: MapboxCommon.LoggingLevel - - /// A Boolean value that indicates whether a copilot recording is enabled. Defaults to `false`. - public let copilotEnabled: Bool - - /// Configures default unit of measurement. - public var unitOfMeasurement: UnitOfMeasurement = .auto - - /// A `Locale` that is used for guidance instruction and other localization features. - public var locale: Locale = .nationalizedCurrent - - /// A Boolean value that indicates whether a background location tracking is enabled. Defaults to `true`. - public let disableBackgroundTrackingLocation: Bool - - /// A Boolean value that indicates whether a sensor data is utilized. Defaults to `false`. - public let utilizeSensorData: Bool - - /// Defines approximate navigator prediction between location ticks. - /// Due to discrete location updates, Navigator always operates data "in the past" so it has to make prediction - /// about user's current real position. This interval controls how far ahead Navigator will try to predict user - /// location. - public let navigatorPredictionInterval: TimeInterval? - - /// Congestion level configuration. - public var congestionConfig: CongestionRangesConfiguration - - /// Configuration for navigation history recording. - public let historyRecordingConfig: HistoryRecordingConfig? - - /// Predictive cache configuration. - public var predictiveCacheConfig: PredictiveCacheConfig? - - /// Electronic Horizon Configuration. - public var electronicHorizonConfig: ElectronicHorizonConfig? - - /// Electronic Horizon incidents configuration. - public let liveIncidentsConfig: IncidentsConfig? - - /// Multileg advancing mode. - public var multilegAdvancing: MultilegAdvanceMode - - /// Tiles version. - public let tilesVersion: String - - /// Options for configuring how map and navigation tiles are stored on the device. - public let tilestoreConfig: TileStoreConfiguration - - /// Configuration for Text-To-Speech engine used. - public var ttsConfig: TTSConfig - - /// Billing handler overriding for testing purposes. - var __customBillingHandler: BillingHandlerProvider? = nil - - /// Events manager overriding for testing purposes. - var __customEventsManager: EventsManagerProvider? = nil - - /// Routing provider overriding for testing purposes. - var __customRoutingProvider: CustomRoutingProvider? = nil - - /// Mutable Routing configuration. - public struct RouteRequestConfig: Equatable, Sendable { - /// A string specifying the primary mode of transportation for the routes. - /// `ProfileIdentifier.automobileAvoidingTraffic` is used by default. - public let profileIdentifier: ProfileIdentifier - - /// The route classes that the calculated routes will avoid. - public var roadClassesToAvoid: RoadClasses - - /// The route classes that the calculated routes will allow. - /// This property has no effect unless the profile identifier is set to `ProfileIdentifier.automobile` or - /// `ProfileIdentifier.automobileAvoidingTraffic`. - public var roadClassesToAllow: RoadClasses - - /// A Boolean value that indicates whether a returned route may require a point U-turn at an intermediate - /// waypoint. - /// - /// If the value of this property is `true`, a returned route may require an immediate U-turn at an - /// intermediate - /// waypoint. At an intermediate waypoint, if the value of this property is `false`, each returned route may - /// continue straight ahead or turn to either side but may not U-turn. This property has no effect if only two - /// waypoints are specified. - /// - /// Set this property to `true` if you expect the user to traverse each leg of the trip separately. For - /// example, it - /// would be quite easy for the user to effectively “U-turn” at a waypoint if the user first parks the car and - /// patronizes a restaurant there before embarking on the next leg of the trip. Set this property to `false` if - /// you - /// expect the user to proceed to the next waypoint immediately upon arrival. For example, if the user only - /// needs to - /// drop off a passenger or package at the waypoint before continuing, it would be inconvenient to perform a - /// U-turn - /// at that location. - /// The default value of this property is `false. - public var allowsUTurnAtWaypoint: Bool - - /// URL query items to be parsed and applied as configuration to the route request. - public var customQueryParameters: [URLQueryItem]? - - /// Initializes a new `CoreConfig` object. - /// - Parameters: - /// - profileIdentifier: A string specifying the primary mode of transportation for the routes. - /// - roadClassesToAvoid: The route classes that the calculated routes will avoid. - /// - roadClassesToAllow: The route classes that the calculated routes will allow. - /// - allowsUTurnAtWaypoint: A Boolean value that indicates whether a returned route may require a point - /// - customQueryParameters: URL query items to be parsed and applied as configuration to the route request. - /// U-turn at an intermediate waypoint. - public init( - profileIdentifier: ProfileIdentifier = .automobileAvoidingTraffic, - roadClassesToAvoid: RoadClasses = [], - roadClassesToAllow: RoadClasses = [], - allowsUTurnAtWaypoint: Bool = false, - customQueryParameters: [URLQueryItem]? = nil - ) { - self.profileIdentifier = profileIdentifier - self.roadClassesToAvoid = roadClassesToAvoid - self.roadClassesToAllow = roadClassesToAllow - self.allowsUTurnAtWaypoint = allowsUTurnAtWaypoint - self.customQueryParameters = customQueryParameters - } - } - - /// Creates a new ``CoreConfig`` instance. - /// - Parameters: - /// - credentials: SDK Credentials. - /// - routeRequestConfig: Route requiest configuration - /// - routingConfig: Routing Configuration. - /// - telemetryAppMetadata: Custom metadata that can be used with events in the telemetry pipeline. - /// - logLevel: Logging level for Mapbox SDKs. - /// - isSimulationEnabled: A Boolean value that indicates whether a route simulation is enabled. - /// - copilotEnabled: A Boolean value that indicates whether a copilot recording is enabled. - /// - unitOfMeasurement: Configures default unit of measurement. - /// - locale: A `Locale` that is used for guidance instruction and other localization features. - /// - disableBackgroundTrackingLocation: Indicates if a background location tracking is enabled. - /// - utilizeSensorData: A Boolean value that indicates whether a sensor data is utilized. - /// - navigatorPredictionInterval: Defines approximate navigator prediction between location ticks. - /// - congestionConfig: Congestion level configuration. - /// - historyRecordingConfig: Configuration for navigation history recording. - /// - predictiveCacheConfig: Predictive cache configuration. - /// - electronicHorizonConfig: Electronic Horizon Configuration. - /// - liveIncidentsConfig: Electronic Horizon incidents configuration. - /// - multilegAdvancing: Multileg advancing mode. - /// - tilesVersion: Tiles version. - /// - tilestoreConfig: Options for configuring how map and navigation tiles are stored on the device. - /// - ttsConfig: Configuration for Text-To-Speech engine used. - public init( - credentials: NavigationCoreApiConfiguration = .init(), - routeRequestConfig: RouteRequestConfig = .init(), - routingConfig: RoutingConfig = .init(), - telemetryAppMetadata: TelemetryAppMetadata? = nil, - logLevel: MapboxCommon.LoggingLevel = .warning, - locationSource: LocationSource = .live, - copilotEnabled: Bool = false, - unitOfMeasurement: UnitOfMeasurement = .auto, - locale: Locale = .nationalizedCurrent, - disableBackgroundTrackingLocation: Bool = true, - utilizeSensorData: Bool = false, - navigatorPredictionInterval: TimeInterval? = nil, - congestionConfig: CongestionRangesConfiguration = .default, - historyRecordingConfig: HistoryRecordingConfig? = nil, - predictiveCacheConfig: PredictiveCacheConfig? = PredictiveCacheConfig(), - electronicHorizonConfig: ElectronicHorizonConfig? = nil, - liveIncidentsConfig: IncidentsConfig? = nil, - multilegAdvancing: MultilegAdvanceMode = .automatically, - tilesVersion: String = "", - tilestoreConfig: TileStoreConfiguration = .default, - ttsConfig: TTSConfig = .default - ) { - self.credentials = credentials - self.routeRequestConfig = routeRequestConfig - self.telemetryAppMetadata = telemetryAppMetadata - self.logLevel = logLevel - self.locationSource = locationSource - self.copilotEnabled = copilotEnabled - self.unitOfMeasurement = unitOfMeasurement - self.locale = locale - self.disableBackgroundTrackingLocation = disableBackgroundTrackingLocation - self.utilizeSensorData = utilizeSensorData - self.navigatorPredictionInterval = navigatorPredictionInterval - self.congestionConfig = congestionConfig - self.historyRecordingConfig = historyRecordingConfig - self.predictiveCacheConfig = predictiveCacheConfig - self.electronicHorizonConfig = electronicHorizonConfig - self.liveIncidentsConfig = liveIncidentsConfig - self.multilegAdvancing = multilegAdvancing - self.routingConfig = routingConfig - self.tilesVersion = tilesVersion - self.tilestoreConfig = tilestoreConfig - self.ttsConfig = ttsConfig - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/UnitOfMeasurement.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/UnitOfMeasurement.swift deleted file mode 100644 index f26bd6923..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/Configuration/UnitOfMeasurement.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -/// Holds available types of measurement units. -public enum UnitOfMeasurement: Equatable, Sendable { - /// Allows SDK to pick proper units. - case auto - /// Selects imperial units as default. - case imperial - /// Selects metric units as default. - case metric -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/CustomRoutingProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/CustomRoutingProvider.swift deleted file mode 100644 index f85a569bf..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/CustomRoutingProvider.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -struct CustomRoutingProvider: Equatable { - static func == (lhs: CustomRoutingProvider, rhs: CustomRoutingProvider) -> Bool { - lhs.object === rhs.object - } - - private var object: RoutingProvider & AnyObject - func callAsFunction() -> RoutingProvider { - return object - } - - init(_ object: RoutingProvider & AnyObject) { - self.object = object - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/EventsManagerProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/EventsManagerProvider.swift deleted file mode 100644 index 783865181..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/EventsManagerProvider.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -struct EventsManagerProvider: Equatable { - static func == (lhs: EventsManagerProvider, rhs: EventsManagerProvider) -> Bool { - return lhs.object === rhs.object - } - - private var object: NavigationEventsManager - func callAsFunction() -> NavigationEventsManager { - return object - } - - init(_ object: NavigationEventsManager) { - self.object = object - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/FasterRouteDetectionConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/FasterRouteDetectionConfig.swift deleted file mode 100644 index 34148d485..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/FasterRouteDetectionConfig.swift +++ /dev/null @@ -1,44 +0,0 @@ -import CoreLocation -import Foundation - -/// Options to configure fetching, detecting, and accepting a faster route during active guidance. -public struct FasterRouteDetectionConfig: Equatable { - public static func == (lhs: FasterRouteDetectionConfig, rhs: FasterRouteDetectionConfig) -> Bool { - guard lhs.fasterRouteApproval == rhs.fasterRouteApproval, - lhs.proactiveReroutingInterval == rhs.proactiveReroutingInterval, - lhs.minimumRouteDurationRemaining == rhs.minimumRouteDurationRemaining, - lhs.minimumManeuverOffset == rhs.minimumManeuverOffset - else { - return false - } - - switch (lhs.customFasterRouteProvider, rhs.customFasterRouteProvider) { - case (.none, .none), (.some(_), .some(_)): - return true - default: - return false - } - } - - public typealias FasterRouteApproval = ApprovalModeAsync<(CLLocation, NavigationRoute)> - - public var fasterRouteApproval: FasterRouteApproval - public var proactiveReroutingInterval: TimeInterval - public var minimumRouteDurationRemaining: TimeInterval - public var minimumManeuverOffset: TimeInterval - public var customFasterRouteProvider: (any FasterRouteProvider)? - - public init( - fasterRouteApproval: FasterRouteApproval = .automatically, - proactiveReroutingInterval: TimeInterval = 120, - minimumRouteDurationRemaining: TimeInterval = 600, - minimumManeuverOffset: TimeInterval = 70, - customFasterRouteProvider: (any FasterRouteProvider)? = nil - ) { - self.fasterRouteApproval = fasterRouteApproval - self.proactiveReroutingInterval = proactiveReroutingInterval - self.minimumRouteDurationRemaining = minimumRouteDurationRemaining - self.minimumManeuverOffset = minimumManeuverOffset - self.customFasterRouteProvider = customFasterRouteProvider - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/HistoryRecordingConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/HistoryRecordingConfig.swift deleted file mode 100644 index 3f579c778..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/HistoryRecordingConfig.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -public struct HistoryRecordingConfig: Equatable, Sendable { - public static let defaultFolderName = "historyRecordings" - - public var historyDirectoryURL: URL - - public init( - historyDirectoryURL: URL = FileManager.default.urls( - for: .documentDirectory, - in: .userDomainMask - )[0].appendingPathComponent(defaultFolderName) - ) { - self.historyDirectoryURL = historyDirectoryURL - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/IncidentsConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/IncidentsConfig.swift deleted file mode 100644 index 1f1d4a0bc..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/IncidentsConfig.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation - -/// Configures how Electronic Horizon supports live incidents on a most probable path. -/// -/// To enable live incidents ``IncidentsConfig`` should be provided to ``CoreConfig/liveIncidentsConfig`` before -/// starting navigation. -public struct IncidentsConfig: Equatable, Sendable { - /// Incidents provider graph name. - /// - /// If empty - incidents will be disabled. - public var graph: String - - /// LTS incidents service API url. - /// - /// If `nil` is supplied will use a default url. - public var apiURL: URL? - - /// Creates new ``IncidentsConfig``. - /// - Parameters: - /// - graph: Incidents provider graph name. - /// - apiURL: LTS incidents service API url. - public init(graph: String, apiURL: URL?) { - self.graph = graph - self.apiURL = apiURL - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/NavigationCoreApiConfiguration.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/NavigationCoreApiConfiguration.swift deleted file mode 100644 index 467eba29e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/NavigationCoreApiConfiguration.swift +++ /dev/null @@ -1,41 +0,0 @@ -import MapboxDirections - -/// Allows to configure access token and endpoint for separate SDK requests separately for directions, maps, and speech -/// requests. -public struct NavigationCoreApiConfiguration: Equatable, Sendable { - /// The configuration used to make directions-related requests. - public let navigation: ApiConfiguration - /// The configuration used to make map-loading requests. - public let map: ApiConfiguration - /// The configuration used to make speech-related requests. - public let speech: ApiConfiguration - - /// Initializes ``NavigationCoreApiConfiguration`` instance. - /// - Parameters: - /// - navigation: The configuration used to make directions-related requests. - /// - map: The configuration used to make map-loading requests. - /// - speech: The configuration used to make speech-related requests. - public init( - navigation: ApiConfiguration = .default, - map: ApiConfiguration = .default, - speech: ApiConfiguration = .default - ) { - self.navigation = navigation - self.map = map - self.speech = speech - } -} - -extension NavigationCoreApiConfiguration { - /// Initializes ``NavigationCoreApiConfiguration`` instance. - /// - Parameter accessToken: A Mapbox [access token](https://www.mapbox.com/help/define-access-token/) used to - /// authorize Mapbox API requests. - public init(accessToken: String) { - let configuration = ApiConfiguration(accessToken: accessToken) - self.init( - navigation: configuration, - map: configuration, - speech: configuration - ) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/RerouteConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/RerouteConfig.swift deleted file mode 100644 index 370d69af3..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/RerouteConfig.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import MapboxDirections - -/// Configures the rerouting behavior. -public struct RerouteConfig: Equatable { - public typealias OptionsCustomization = EquatableClosure - - /// Optional customization callback triggered on reroute attempts. - /// - /// Provide this callback if you need to modify route request options, done during building a reroute. This will - /// not affect initial route requests. - public var optionsCustomization: OptionsCustomization? - /// Enables or disables rerouting mechanism. - /// - /// Disabling rerouting will result in the route remaining unchanged even if the user wanders off of it. - /// Reroute detecting is enabled by default. - public var detectsReroute: Bool - - public init( - detectsReroute: Bool = true, - optionsCustomization: OptionsCustomization? = nil - ) { - self.detectsReroute = detectsReroute - self.optionsCustomization = optionsCustomization - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/RoutingConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/RoutingConfig.swift deleted file mode 100644 index e9335c340..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/RoutingConfig.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation - -/// Routing Configuration. -public struct RoutingConfig: Equatable { - /// Options to configure fetching, detecting, and accepting ``AlternativeRoute``s during navigation. - /// - /// Use `nil` value to disable the mechanism - public var alternativeRoutesDetectionConfig: AlternativeRoutesDetectionConfig? - - /// Options to configure fetching, detecting, and accepting a faster route during active guidance. - /// - /// Use `nil` value to disable the mechanism. - public var fasterRouteDetectionConfig: FasterRouteDetectionConfig? - - /// Configures the rerouting behavior. - public var rerouteConfig: RerouteConfig - - /// A radius around the current user position in which the API will avoid returning any significant maneuvers when - /// rerouting or suggesting alternative routes. - /// Provided `TimeInterval` value will be converted to meters using current speed. Default value is `8` seconds. - public var initialManeuverAvoidanceRadius: TimeInterval - - /// A time interval in which time-dependent properties of the ``RouteLeg``s of the resulting `Route`s will be - /// refreshed. - /// - /// This property is ignored unless ``profileIdentifier`` is `ProfileIdentifier.automobileAvoidingTraffic`. - /// Use `nil` value to disable the mechanism. - public var routeRefreshPeriod: TimeInterval? - - /// Type of routing to be used by various SDK objects when providing route calculations. Use this value to configure - /// online vs. offline data usage for routing. - /// - /// Default value is ``RoutingProviderSource/hybrid`` - public var routingProviderSource: RoutingProviderSource - - /// Enables automatic switching to online version of the current route when possible. - /// - /// Indicates if ``NavigationController`` will attempt to detect if thr current route was build offline and if there - /// is an online route with the same path is available to automatically switch to it. Using online route is - /// beneficial due to available live data like traffic congestion, incidents, etc. Check is not performed instantly - /// and it is not guaranteed to receive an online version at any given period of time. - /// - /// Enabled by default. - public var prefersOnlineRoute: Bool - - @available( - *, - deprecated, - message: "Use 'init(alternativeRoutesDetectionConfig:fasterRouteDetectionConfig:rerouteConfig:initialManeuverAvoidanceRadius:routeRefreshPeriod:routingProviderSource:prefersOnlineRoute:)' instead." - ) - public init( - alternativeRoutesDetectionSettings: AlternativeRoutesDetectionConfig? = .init(), - fasterRouteDetectionSettings: FasterRouteDetectionConfig? = .init(), - rerouteSettings: RerouteConfig = .init(), - initialManeuverAvoidanceRadius: TimeInterval = 8, - routeRefreshPeriod: TimeInterval? = 120, - routingProviderSource: RoutingProviderSource = .hybrid, - prefersOnlineRoute: Bool = true, - detectsReroute: Bool = true - ) { - self.alternativeRoutesDetectionConfig = alternativeRoutesDetectionSettings - self.fasterRouteDetectionConfig = fasterRouteDetectionSettings - self.rerouteConfig = rerouteSettings - self.initialManeuverAvoidanceRadius = initialManeuverAvoidanceRadius - self.routeRefreshPeriod = routeRefreshPeriod - self.routingProviderSource = routingProviderSource - self.prefersOnlineRoute = prefersOnlineRoute - } - - public init( - alternativeRoutesDetectionConfig: AlternativeRoutesDetectionConfig? = .init(), - fasterRouteDetectionConfig: FasterRouteDetectionConfig? = .init(), - rerouteConfig: RerouteConfig = .init(), - initialManeuverAvoidanceRadius: TimeInterval = 8, - routeRefreshPeriod: TimeInterval? = 120, - routingProviderSource: RoutingProviderSource = .hybrid, - prefersOnlineRoute: Bool = true - ) { - self.alternativeRoutesDetectionConfig = alternativeRoutesDetectionConfig - self.fasterRouteDetectionConfig = fasterRouteDetectionConfig - self.rerouteConfig = rerouteConfig - self.initialManeuverAvoidanceRadius = initialManeuverAvoidanceRadius - self.routeRefreshPeriod = routeRefreshPeriod - self.routingProviderSource = routingProviderSource - self.prefersOnlineRoute = prefersOnlineRoute - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/SettingsWrappers.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/SettingsWrappers.swift deleted file mode 100644 index a27226461..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/SettingsWrappers.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation - -public enum ApprovalMode: Equatable, Sendable { - case automatically - case manually -} - -public enum ApprovalModeAsync: Equatable, Sendable { - public static func == (lhs: ApprovalModeAsync, rhs: ApprovalModeAsync) -> Bool { - switch (lhs, rhs) { - case (.automatically, .automatically), - (.manually(_), .manually(_)): - return true - default: - return false - } - } - - public typealias ApprovalCheck = @Sendable (Context) async -> Bool - - case automatically - case manually(ApprovalCheck) -} - -public struct EquatableClosure: Equatable { - public static func == (lhs: EquatableClosure, rhs: EquatableClosure) -> Bool { - return (lhs.closure != nil) == (rhs.closure != nil) - } - - public typealias Closure = (Input) -> Output - private var closure: Closure? - - func callAsFunction(_ input: Input) -> Output? { - return closure?(input) - } - - public init(_ closure: Closure? = nil) { - self.closure = closure - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/StatusUpdatingSettings.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/StatusUpdatingSettings.swift deleted file mode 100644 index 9fb3bb27c..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/StatusUpdatingSettings.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation - -/// Configures Navigator status polling. -public struct StatusUpdatingSettings { - /// If new location is not provided during ``updatingPatience`` - status will be polled unconditionally. - /// - /// If `nil` - default value will be used. - public var updatingPatience: TimeInterval? - /// Interval of unconditional status polling. - /// - /// If `nil` - default value will be used. - public var updatingInterval: TimeInterval? - - /// Creates new ``StatusUpdatingSettings``. - /// - Parameters: - /// - updatingPatience: The patience time before unconditional status polling. - /// - updatingInterval: The unconditional polling interval. - public init(updatingPatience: TimeInterval? = nil, updatingInterval: TimeInterval? = nil) { - self.updatingPatience = updatingPatience - self.updatingInterval = updatingInterval - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/TTSConfig.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/TTSConfig.swift deleted file mode 100644 index 16b80fcab..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/TTSConfig.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -/// Text-To-Speech configuration. -public enum TTSConfig: Equatable, Sendable { - public static func == (lhs: TTSConfig, rhs: TTSConfig) -> Bool { - switch (lhs, rhs) { - case (.default, .default), - (.localOnly, .localOnly), - (.custom, .custom): - return true - default: - return false - } - } - - case `default` - case localOnly - case custom(speechSynthesizer: SpeechSynthesizing) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/TelemetryAppMetadata.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/TelemetryAppMetadata.swift deleted file mode 100644 index 6690dff96..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/TelemetryAppMetadata.swift +++ /dev/null @@ -1,48 +0,0 @@ -import CoreLocation -import Foundation - -/// Custom metadata that can be used with events in the telemetry pipeline. -public struct TelemetryAppMetadata: Equatable, Sendable { - /// Name of the application. - public let name: String - /// Version of the application. - public let version: String - /// User ID relevant for the application context. - public var userId: String? - /// Session ID relevant for the application context. - public var sessionId: String? - - /// nitializes a new `TelemetryAppMetadata` object. - /// - Parameters: - /// - name: Name of the application. - /// - version: Version of the application. - /// - userId: User ID relevant for the application context. - /// - sessionId: Session ID relevant for the application context. - public init( - name: String, - version: String, - userId: String?, - sessionId: String? - ) { - self.name = name - self.version = version - self.userId = userId - self.sessionId = sessionId - } -} - -extension TelemetryAppMetadata { - var configuration: [String: String?] { - var dictionary: [String: String?] = [ - "name": name, - "version": version, - ] - if let userId, !userId.isEmpty { - dictionary["userId"] = userId - } - if let sessionId, !sessionId.isEmpty { - dictionary["sessionId"] = sessionId - } - return dictionary - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Settings/TileStoreConfiguration.swift b/ios/Classes/Navigation/MapboxNavigationCore/Settings/TileStoreConfiguration.swift deleted file mode 100644 index 5736f6877..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Settings/TileStoreConfiguration.swift +++ /dev/null @@ -1,65 +0,0 @@ -import Foundation -import MapboxCommon - -/// Options for configuring how map and navigation tiles are stored on the device. -/// -/// This struct encapsulates logic for handling ``TileStoreConfiguration/Location/default`` and -/// ``TileStoreConfiguration/Location/custom(_:)`` paths as well as providing corresponding `TileStore`s. -/// It also covers differences between tile storages for Map and Navigation data. Tupically, you won't need to configure -/// these and rely on defaults, unless you provide pre-downloaded data withing your app in which case you'll need -/// ``TileStoreConfiguration/Location/custom(_:)`` path to point to your data. -public struct TileStoreConfiguration: Equatable, Sendable { - /// Describes filesystem location for tile storage folder - public enum Location: Equatable, Sendable { - /// Encapsulated default location. - /// - /// ``tileStoreURL`` for this case will return `nil`. - case `default` - /// User-provided path to tile storage folder. - case custom(URL) - - /// Corresponding URL path. - /// - /// ``TileStoreConfiguration/Location/default`` location is interpreted as `nil`. - public var tileStoreURL: URL? { - switch self { - case .default: - return nil - case .custom(let url): - return url - } - } - - /// A `TileStore` instance, configured for current location. - public var tileStore: TileStore { - switch self { - case .default: - return TileStore.__create() - case .custom(let url): - return TileStore.__create(forPath: url.path) - } - } - } - - /// Location of Navigator tiles data. - public let navigatorLocation: Location - - /// Location of Map tiles data. - public let mapLocation: Location? - - /// Tile data will be stored at default SDK location. - public static var `default`: Self { - .init(navigatorLocation: .default, mapLocation: .default) - } - - /// Custom path to a folder, where tiles data will be stored. - public static func custom(_ url: URL) -> Self { - .init(navigatorLocation: .custom(url), mapLocation: .custom(url)) - } - - /// Option to configure Map and Navigation tiles to be stored separately. You should not use this option unless you - /// know what you are doing. - public static func isolated(navigationLocation: Location, mapLocation: Location?) -> Self { - .init(navigatorLocation: navigationLocation, mapLocation: mapLocation) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/ConnectivityTypeProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/ConnectivityTypeProvider.swift deleted file mode 100644 index 37eb4b31e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/ConnectivityTypeProvider.swift +++ /dev/null @@ -1,75 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import Network - -protocol ConnectivityTypeProvider { - var connectivityType: String { get } -} - -protocol NetworkMonitor: AnyObject, Sendable { - func start(queue: DispatchQueue) - var pathUpdateHandler: (@Sendable (_ newPath: NWPath) -> Void)? { get set } -} - -protocol NetworkPath { - var status: NWPath.Status { get } - func usesInterfaceType(_ type: NWInterface.InterfaceType) -> Bool -} - -extension NWPathMonitor: NetworkMonitor {} -extension NWPath: NetworkPath {} -extension NWPathMonitor: @unchecked Sendable {} - -final class MonitorConnectivityTypeProvider: ConnectivityTypeProvider, Sendable { - private let monitor: NetworkMonitor - private let monitorConnectionType = UnfairLocked(nil) - private let queue = DispatchQueue.global(qos: .utility) - - private static let connectionTypes: [NWInterface.InterfaceType] = [.cellular, .wifi, .wiredEthernet] - - var connectivityType: String { - monitorConnectionType.read()?.connectionType ?? "No Connection" - } - - init(monitor: NetworkMonitor = NWPathMonitor()) { - self.monitor = monitor - - configureMonitor() - } - - func handleChange(to path: NetworkPath) { - let newMonitorConnectionType: NWInterface.InterfaceType? - if path.status == .satisfied { - let connectionTypes = MonitorConnectivityTypeProvider.connectionTypes - newMonitorConnectionType = connectionTypes.first(where: path.usesInterfaceType) ?? .other - } else { - newMonitorConnectionType = nil - } - monitorConnectionType.update(newMonitorConnectionType) - } - - private func configureMonitor() { - monitor.pathUpdateHandler = { [weak self] path in - self?.handleChange(to: path) - } - monitor.start(queue: queue) - } -} - -extension NWInterface.InterfaceType { - fileprivate var connectionType: String { - switch self { - case .cellular: - return "Cellular" - case .wifi: - return "WiFi" - case .wiredEthernet: - return "Wired" - case .loopback, .other: - return "Unknown" - @unknown default: - Log.warning("Unexpected NWInterface.InterfaceType type", category: .settings) - return "Unexpected" - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventAppState.swift b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventAppState.swift deleted file mode 100644 index 23f238bf3..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventAppState.swift +++ /dev/null @@ -1,153 +0,0 @@ -import _MapboxNavigationHelpers -import UIKit - -final class EventAppState: Sendable { - struct Environment { - let date: @Sendable () -> Date - let applicationState: @Sendable () -> UIApplication.State - let screenOrientation: @Sendable () -> UIDeviceOrientation - let deviceOrientation: @Sendable () -> UIDeviceOrientation - - static var live: Self { - .init( - date: { Date() }, - applicationState: { - onMainQueueSync { - UIApplication.shared.applicationState - } - }, - screenOrientation: { - onMainQueueSync { - UIDevice.current.screenOrientation - } - }, - deviceOrientation: { - onMainQueueSync { - UIDevice.current.orientation - } - } - ) - } - } - - private let environment: Environment - private let innerState: UnfairLocked - private let sessionStarted: Date - - private struct State { - var timeSpentInPortrait: TimeInterval = 0 - var lastOrientation: UIDeviceOrientation - var lastTimeOrientationChanged: Date - - var timeInBackground: TimeInterval = 0 - var lastTimeEnteredBackground: Date? - } - - var percentTimeInForeground: Int { - let state = innerState.read() - let currentDate = environment.date() - var totalTimeInBackground = state.timeInBackground - if let lastTimeEnteredBackground = state.lastTimeEnteredBackground { - totalTimeInBackground += currentDate.timeIntervalSince(lastTimeEnteredBackground) - } - - let totalTime = currentDate.timeIntervalSince(sessionStarted) - return totalTime > 0 ? Int(100 * (totalTime - totalTimeInBackground) / totalTime) : 100 - } - - var percentTimeInPortrait: Int { - let state = innerState.read() - let currentDate = environment.date() - var totalTimeInPortrait = state.timeSpentInPortrait - if state.lastOrientation.isPortrait { - totalTimeInPortrait += currentDate.timeIntervalSince(state.lastTimeOrientationChanged) - } - - let totalTime = currentDate.timeIntervalSince(sessionStarted) - return totalTime > 0 ? Int(100 * totalTimeInPortrait / totalTime) : 100 - } - - @MainActor - init(environment: Environment = .live) { - self.environment = environment - - let date = environment.date() - self.sessionStarted = date - let lastOrientation = environment.screenOrientation() - let lastTimeOrientationChanged = date - let lastTimeEnteredBackground: Date? = environment.applicationState() == .background ? date : nil - let innerState = State( - lastOrientation: lastOrientation, - lastTimeOrientationChanged: lastTimeOrientationChanged, - lastTimeEnteredBackground: lastTimeEnteredBackground - ) - self.innerState = .init(innerState) - - subscribeNotifications() - } - - // MARK: - State Management - - private func subscribeNotifications() { - NotificationCenter.default.addObserver( - self, - selector: #selector(willEnterForegroundState), - name: UIApplication.willEnterForegroundNotification, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(didEnterBackgroundState), - name: UIApplication.didEnterBackgroundNotification, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(didChangeOrientation), - name: UIDevice.orientationDidChangeNotification, - object: nil - ) - } - - @objc - private func didChangeOrientation() { - handleOrientationChange() - } - - @objc - private func didEnterBackgroundState() { - let date = environment.date() - innerState.mutate { - $0.lastTimeEnteredBackground = date - } - } - - @objc - private func willEnterForegroundState() { - let state = innerState.read() - guard let dateEnteredBackground = state.lastTimeEnteredBackground else { return } - - let timeDelta = environment.date().timeIntervalSince(dateEnteredBackground) - innerState.mutate { - $0.timeInBackground += timeDelta - $0.lastTimeEnteredBackground = nil - } - } - - private func handleOrientationChange() { - let state = innerState.read() - let orientation = environment.deviceOrientation() - guard orientation.isValidInterfaceOrientation else { return } - guard state.lastOrientation.isPortrait != orientation.isPortrait || - state.lastOrientation.isLandscape != orientation.isLandscape else { return } - - let currentDate = environment.date() - let timePortraitDelta = orientation.isLandscape ? currentDate - .timeIntervalSince(state.lastTimeOrientationChanged) : 0 - innerState.mutate { - $0.timeSpentInPortrait += timePortraitDelta - $0.lastTimeOrientationChanged = currentDate - $0.lastOrientation = orientation - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventsMetadataProvider.swift b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventsMetadataProvider.swift deleted file mode 100644 index 3cbc80b56..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/EventsMetadataProvider.swift +++ /dev/null @@ -1,176 +0,0 @@ -import _MapboxNavigationHelpers -import AVFoundation -import MapboxNavigationNative -import UIKit - -protocol AudioSessionInfoProvider { - var outputVolume: Float { get } - var telemetryAudioType: AudioType { get } -} - -final class EventsMetadataProvider: EventsMetadataInterface, Sendable { - let _userInfo: UnfairLocked<[String: String?]?> - var userInfo: [String: String?]? { - get { - _userInfo.read() - } - set { - _userInfo.update(newValue) - } - } - - private let screen: UIScreen - private let audioSessionInfoProvider: UnfairLocked - private let device: UIDevice - private let connectivityTypeProvider: UnfairLocked - private let appState: EventAppState - - @MainActor - init( - appState: EventAppState, - screen: UIScreen, - audioSessionInfoProvider: AudioSessionInfoProvider = AVAudioSession.sharedInstance(), - device: UIDevice, - connectivityTypeProvider: ConnectivityTypeProvider = MonitorConnectivityTypeProvider() - ) { - self.appState = appState - self.screen = screen - self.audioSessionInfoProvider = .init(audioSessionInfoProvider) - self.device = device - self.connectivityTypeProvider = .init(connectivityTypeProvider) - self._userInfo = .init(nil) - - device.isBatteryMonitoringEnabled = true - - self.batteryLevel = .init(Self.currentBatteryLevel(with: device)) - self.batteryPluggedIn = .init(Self.currentBatteryPluggedIn(with: device)) - self.screenBrightness = .init(Int(screen.brightness * 100)) - - let notificationCenter = NotificationCenter.default - notificationCenter.addObserver( - self, - selector: #selector(batteryLevelDidChange), - name: UIDevice.batteryLevelDidChangeNotification, - object: nil - ) - notificationCenter.addObserver( - self, - selector: #selector(batteryStateDidChange), - name: UIDevice.batteryStateDidChangeNotification, - object: nil - ) - notificationCenter.addObserver( - self, - selector: #selector(brightnessDidChange), - name: UIScreen.brightnessDidChangeNotification, - object: nil - ) - } - - private var appMetadata: AppMetadata? { - guard let userInfo, - let appName = userInfo["name"] as? String, - let appVersion = userInfo["version"] as? String else { return nil } - - return AppMetadata( - name: appName, - version: appVersion, - userId: userInfo["userId"] as? String, - sessionId: userInfo["sessionId"] as? String - ) - } - - private let screenBrightness: UnfairLocked - private var volumeLevel: Int { Int(audioSessionInfoProvider.read().outputVolume * 100) } - private var audioType: AudioType { audioSessionInfoProvider.read().telemetryAudioType } - - private let batteryPluggedIn: UnfairLocked - private let batteryLevel: UnfairLocked - private var connectivity: String { connectivityTypeProvider.read().connectivityType } - - func provideEventsMetadata() -> EventsMetadata { - return EventsMetadata( - volumeLevel: volumeLevel as NSNumber, - audioType: audioType.rawValue as NSNumber, - screenBrightness: screenBrightness.read() as NSNumber, - percentTimeInForeground: appState.percentTimeInForeground as NSNumber, - percentTimeInPortrait: appState.percentTimeInPortrait as NSNumber, - batteryPluggedIn: batteryPluggedIn.read() as NSNumber, - batteryLevel: batteryLevel.read() as NSNumber?, - connectivity: connectivity, - appMetadata: appMetadata - ) - } - - @objc - private func batteryLevelDidChange() { - let newValue = Self.currentBatteryLevel(with: device) - batteryLevel.update(newValue) - } - - private static func currentBatteryLevel(with device: UIDevice) -> Int? { - device.batteryLevel >= 0 ? Int(device.batteryLevel * 100) : nil - } - - @objc - private func batteryStateDidChange() { - let newValue = Self.currentBatteryPluggedIn(with: device) - batteryPluggedIn.update(newValue) - } - - private static let chargingStates: [UIDevice.BatteryState] = [.charging, .full] - - private static func currentBatteryPluggedIn(with device: UIDevice) -> Bool { - chargingStates.contains(device.batteryState) - } - - @objc - private func brightnessDidChange() { - screenBrightness.update(Int(screen.brightness * 100)) - } -} - -extension AVAudioSession: AudioSessionInfoProvider { - var telemetryAudioType: AudioType { - if currentRoute.outputs - .contains(where: { [.bluetoothA2DP, .bluetoothHFP, .bluetoothLE].contains($0.portType) }) - { - return .bluetooth - } - if currentRoute.outputs - .contains(where: { [.headphones, .airPlay, .HDMI, .lineOut, .carAudio, .usbAudio].contains($0.portType) }) - { - return .headphones - } - if currentRoute.outputs.contains(where: { [.builtInSpeaker, .builtInReceiver].contains($0.portType) }) { - return .speaker - } - return .unknown - } -} - -private final class BlockingOperation: @unchecked Sendable { - private var result: Result? - - func run(_ operation: @Sendable @escaping () async -> T) -> T? { - Task { - let task = Task(operation: operation) - self.result = await task.result - } - DispatchQueue.global().sync { - while result == nil { - RunLoop.current.run(mode: .default, before: .distantFuture) - } - } - switch result { - case .success(let value): - return value - case .none: - assertionFailure("Running blocking operation did not receive a value.") - return nil - } - } -} - -extension EventsMetadata: @unchecked Sendable {} -extension ScreenshotFormat: @unchecked Sendable {} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationNativeEventsManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationNativeEventsManager.swift deleted file mode 100644 index 5aead1a6d..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationNativeEventsManager.swift +++ /dev/null @@ -1,174 +0,0 @@ -import _MapboxNavigationHelpers -import CoreLocation -import Foundation -import MapboxCommon -import MapboxNavigationNative_Private -import UIKit - -final class NavigationNativeEventsManager: NavigationTelemetryManager, Sendable { - private let eventsMetadataProvider: EventsMetadataProvider - private let telemetry: UnfairLocked - - private let _userInfo: UnfairLocked<[String: String?]?> - var userInfo: [String: String?]? { - get { - _userInfo.read() - } - set { - _userInfo.update(newValue) - eventsMetadataProvider.userInfo = newValue - } - } - - required init(eventsMetadataProvider: EventsMetadataProvider, telemetry: Telemetry) { - self.eventsMetadataProvider = eventsMetadataProvider - self.telemetry = .init(telemetry) - self._userInfo = .init(nil) - } - - func createFeedback(screenshotOption: FeedbackScreenshotOption) async -> FeedbackEvent? { - let userFeedbackHandle = telemetry.read().startBuildUserFeedbackMetadata() - let screenshot = await createScreenshot(screenshotOption: screenshotOption) - let feedbackMetadata = FeedbackMetadata(userFeedbackHandle: userFeedbackHandle, screenshot: screenshot) - return FeedbackEvent(metadata: feedbackMetadata) - } - - func sendActiveNavigationFeedback( - _ feedback: FeedbackEvent, - type: ActiveNavigationFeedbackType, - description: String?, - source: FeedbackSource - ) async throws -> UserFeedback { - return try await sendNavigationFeedback( - feedback, - type: type, - description: description, - source: source - ) - } - - func sendPassiveNavigationFeedback( - _ feedback: FeedbackEvent, - type: PassiveNavigationFeedbackType, - description: String?, - source: FeedbackSource - ) async throws -> UserFeedback { - return try await sendNavigationFeedback( - feedback, - type: type, - description: description, - source: source - ) - } - - func sendNavigationFeedback( - _ feedback: FeedbackEvent, - type: FeedbackType, - description: String?, - source: FeedbackSource - ) async throws -> UserFeedback { - let feedbackMetadata = feedback.metadata - guard let userFeedbackMetadata = feedbackMetadata.userFeedbackMetadata else { - throw NavigationEventsManagerError.invalidData - } - - let userFeedback = makeUserFeedback( - feedbackMetadata: feedbackMetadata, - type: type, - description: description, - source: source - ) - return try await withCheckedThrowingContinuation { continuation in - telemetry.read().postUserFeedback( - for: userFeedbackMetadata, - userFeedback: userFeedback - ) { expected in - if expected.isValue(), let coordinate = expected.value { - let userFeedback: UserFeedback = .init( - description: description, - type: type, - source: source, - screenshot: feedbackMetadata.screenshot, - location: CLLocation(coordinate: coordinate.value) - ) - continuation.resume(returning: userFeedback) - } else if expected.isError(), let errorString = expected.error { - continuation - .resume(throwing: NavigationEventsManagerError.failedToSend(reason: errorString as String)) - } else { - continuation.resume(throwing: NavigationEventsManagerError.failedToSend(reason: "Unknown")) - } - } - } - } - - func sendCarPlayConnectEvent() { - telemetry.read().postOuterDeviceEvent(for: .connected) - } - - func sendCarPlayDisconnectEvent() { - telemetry.read().postOuterDeviceEvent(for: .disconnected) - } - - private func createNativeUserCallback( - feedbackMetadata: FeedbackMetadata, - continuation: UnsafeContinuation, - type: FeedbackType, - description: String?, - source: FeedbackSource - ) -> MapboxNavigationNative_Private.UserFeedbackCallback { - return { expected in - if expected.isValue(), let coordinate = expected.value { - let userFeedback: UserFeedback = .init( - description: description, - type: type, - source: source, - screenshot: feedbackMetadata.screenshot, - location: CLLocation(coordinate: coordinate.value) - ) - continuation.resume(returning: userFeedback) - } else if expected.isError(), let errorString = expected.error { - continuation.resume(throwing: NavigationEventsManagerError.failedToSend(reason: errorString as String)) - } - } - } - - private func createScreenshot(screenshotOption: FeedbackScreenshotOption) async -> String? { - let screenshot: UIImage? = switch screenshotOption { - case .automatic: - await captureScreen(scaledToFit: 250) - case .custom(let customScreenshot): - customScreenshot - } - return screenshot?.jpegData(compressionQuality: 0.2)?.base64EncodedString() - } - - private func makeUserFeedback( - feedbackMetadata: FeedbackMetadata, - type: FeedbackType, - description: String?, - source: FeedbackSource - ) -> MapboxNavigationNative_Private.UserFeedback { - var feedbackSubType: [String] = [] - if let subtypeKey = type.subtypeKey { - feedbackSubType.append(subtypeKey) - } - return .init( - feedbackType: type.typeKey, - feedbackSubType: feedbackSubType, - description: description ?? "", - screenshot: .init(jpeg: nil, base64: feedbackMetadata.screenshot) - ) - } -} - -extension String { - func toDataRef() -> DataRef? { - if let data = data(using: .utf8), - let encodedData = Data(base64Encoded: data) - { - return .init(data: encodedData) - } - return nil - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationTelemetryManager.swift b/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationTelemetryManager.swift deleted file mode 100644 index f002ba379..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Telemetry/NavigationTelemetryManager.swift +++ /dev/null @@ -1,42 +0,0 @@ -import CoreLocation -import Foundation - -public struct UserFeedback: @unchecked Sendable { - public let description: String? - public let type: FeedbackType - public let source: FeedbackSource - public let screenshot: String? - public let location: CLLocation -} - -/// The ``NavigationTelemetryManager`` is responsible for telemetry in Navigation. -protocol NavigationTelemetryManager: AnyObject, Sendable { - var userInfo: [String: String?]? { get set } - - func sendCarPlayConnectEvent() - - func sendCarPlayDisconnectEvent() - - func createFeedback(screenshotOption: FeedbackScreenshotOption) async -> FeedbackEvent? - - func sendActiveNavigationFeedback( - _ feedback: FeedbackEvent, - type: ActiveNavigationFeedbackType, - description: String?, - source: FeedbackSource - ) async throws -> UserFeedback - - func sendPassiveNavigationFeedback( - _ feedback: FeedbackEvent, - type: PassiveNavigationFeedbackType, - description: String?, - source: FeedbackSource - ) async throws -> UserFeedback - - func sendNavigationFeedback( - _ feedback: FeedbackEvent, - type: FeedbackType, - description: String?, - source: FeedbackSource - ) async throws -> UserFeedback -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Typealiases.swift b/ios/Classes/Navigation/MapboxNavigationCore/Typealiases.swift deleted file mode 100644 index 239c54186..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Typealiases.swift +++ /dev/null @@ -1,32 +0,0 @@ -import MapboxDirections - -/// Options determining the primary mode of transportation. -public typealias ProfileIdentifier = MapboxDirections.ProfileIdentifier -/// A ``Waypoint`` object indicates a location along a route. It may be the route’s origin or destination, or it may be -/// another location that the route visits. A waypoint object indicates the location’s geographic location along with -/// other optional information, such as a name or the user’s direction approaching the waypoint. -public typealias Waypoint = MapboxDirections.Waypoint -/// A ``CongestionLevel`` indicates the level of traffic congestion along a road segment relative to the normal flow of -/// traffic along that segment. You can color-code a route line according to the congestion level along each segment of -/// the route. -public typealias CongestionLevel = MapboxDirections.CongestionLevel -/// Option set that contains attributes of a road segment. -public typealias RoadClasses = MapboxDirections.RoadClasses - -/// An instruction about an upcoming ``RouteStep``’s maneuver, optimized for speech synthesis. -public typealias SpokenInstruction = MapboxDirections.SpokenInstruction -/// A visual instruction banner contains all the information necessary for creating a visual cue about a given -/// ``RouteStep``. -public typealias VisualInstructionBanner = MapboxDirections.VisualInstructionBanner -/// An error that occurs when calculating directions. -public typealias DirectionsError = MapboxDirections.DirectionsError - -/// A ``RouteLeg`` object defines a single leg of a route between two waypoints. If the overall route has only two -/// waypoints, it has a single ``RouteLeg`` object that covers the entire route. The route leg object includes -/// information about the leg, such as its name, distance, and expected travel time. Depending on the criteria used to -/// calculate the route, the route leg object may also include detailed turn-by-turn instructions. -public typealias RouteLeg = MapboxDirections.RouteLeg -/// A ``RouteStep`` object represents a single distinct maneuver along a route and the approach to the next maneuver. -/// The route step object corresponds to a single instruction the user must follow to complete a portion of the route. -/// For example, a step might require the user to turn then follow a road. -public typealias RouteStep = MapboxDirections.RouteStep diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Utils/NavigationLog.swift b/ios/Classes/Navigation/MapboxNavigationCore/Utils/NavigationLog.swift deleted file mode 100644 index c2238ca0f..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Utils/NavigationLog.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import MapboxCommon_Private.MBXLog_Internal -import OSLog - -@_documentation(visibility: internal) -public typealias Log = NavigationLog - -@_documentation(visibility: internal) -public enum NavigationLog { - public typealias Category = NavigationLogCategory - private typealias Logger = MapboxCommon_Private.Log - - public static func debug(_ message: String, category: Category) { - Logger.debug(forMessage: message, category: category.rawLogCategory) - } - - public static func info(_ message: String, category: Category) { - Logger.info(forMessage: message, category: category.rawLogCategory) - } - - public static func warning(_ message: String, category: Category) { - Logger.warning(forMessage: message, category: category.rawLogCategory) - } - - public static func error(_ message: String, category: Category) { - Logger.error(forMessage: message, category: category.rawLogCategory) - } - - public static func fault(_ message: String, category: Category) { - let faultLog: OSLog = .init(subsystem: "com.mapbox.navigation", category: category.rawValue) - os_log("%{public}@", log: faultLog, type: .fault, message) - Logger.error(forMessage: message, category: category.rawLogCategory) - } - - @available(*, unavailable, message: "Use NavigationLog.debug(_:category:)") - public static func trace(_ message: String) {} -} - -@_documentation(visibility: internal) -public struct NavigationLogCategory: RawRepresentable, Sendable { - public let rawValue: String - - public init(rawValue: String) { - self.rawValue = rawValue - } - - public static let billing: Self = .init(rawValue: "billing") - public static let navigation: Self = .init(rawValue: "navigation") - public static let settings: Self = .init(rawValue: "settings") - public static let unimplementedMethods: Self = .init(rawValue: "unimplemented-methods") - public static let navigationUI: Self = .init(rawValue: "navigation-ui") - public static let carPlay: Self = .init(rawValue: "car-play") - public static let copilot: Self = .init(rawValue: "copilot") - - public var rawLogCategory: String { - "navigation-ios/\(rawValue)" - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Utils/ScreenCapture.swift b/ios/Classes/Navigation/MapboxNavigationCore/Utils/ScreenCapture.swift deleted file mode 100644 index 1986f7c41..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Utils/ScreenCapture.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation -#if os(iOS) -import UIKit - -extension UIWindow { - /// Returns a screenshot of the current window - public func capture() -> UIImage? { - UIGraphicsBeginImageContextWithOptions(frame.size, isOpaque, UIScreen.main.scale) - - drawHierarchy(in: bounds, afterScreenUpdates: false) - - guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil } - - UIGraphicsEndImageContext() - - return image - } -} - -extension UIImage { - func scaled(toFit newWidth: CGFloat) -> UIImage? { - let factor = newWidth / size.width - let newSize = CGSize(width: size.width * factor, height: size.height * factor) - - UIGraphicsBeginImageContext(newSize) - - draw(in: CGRect(origin: .zero, size: newSize)) - - guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil } - - UIGraphicsEndImageContext() - - return image - } -} - -#endif - -@MainActor -func captureScreen(scaledToFit width: CGFloat) -> UIImage? { -#if os(iOS) - return UIApplication.shared.windows.filter(\.isKeyWindow).first?.capture()?.scaled(toFit: width) -#else - - return nil // Not yet implemented for other platforms -#endif -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Utils/UnimplementedLogging.swift b/ios/Classes/Navigation/MapboxNavigationCore/Utils/UnimplementedLogging.swift deleted file mode 100644 index 5a5f9958e..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Utils/UnimplementedLogging.swift +++ /dev/null @@ -1,98 +0,0 @@ -import Dispatch -import Foundation -import OSLog - -/// Protocols that provide no-op default method implementations can use this protocol to log a message to the console -/// whenever an unimplemented delegate method is called. -/// -/// In Swift, optional protocol methods exist only for Objective-C compatibility. However, various protocols in this -/// library follow a classic Objective-C delegate pattern in which the protocol would have a number of optional methods. -/// Instead of the disallowed `optional` keyword, these protocols conform to the ``UnimplementedLogging`` protocol to -/// inform about unimplemented methods at runtime. These console messages are logged to the subsystem `com.mapbox.com` -/// with a category of the format “delegation.ProtocolName”, where ProtocolName is the name of the -/// protocol that defines the method. -/// -/// The default method implementations should be provided as part of the protocol or an extension thereof. If the -/// default implementations reside in an extension, the extension should have the same visibility level as the protocol -/// itself. -public protocol UnimplementedLogging { - /// Prints a warning to standard output. - /// - Parameters: - /// - protocolType: The type of the protocol to implement. - /// - level: The log level. - /// - function: The function name to be logged. - func logUnimplemented(protocolType: Any, level: OSLogType, function: String) -} - -extension UnimplementedLogging { - public func logUnimplemented(protocolType: Any, level: OSLogType, function: String = #function) { - let protocolDescription = String(describing: protocolType) - let selfDescription = String(describing: type(of: self)) - - let description = UnimplementedLoggingState.Description(typeDescription: selfDescription, function: function) - - guard _unimplementedLoggingState.markWarned(description) == .marked else { - return - } - - let logMethod: (String, NavigationLogCategory) -> Void = switch level { - case .debug, .info: - Log.info - case .fault: - Log.fault - case .error: - Log.error - default: - Log.warning - } - logMethod( - "Unimplemented delegate method in \(selfDescription): \(protocolDescription).\(function). This message will only be logged once.", - .unimplementedMethods - ) - } -} - -/// Contains a list of unimplemented log descriptions so that we won't log the same warnings twice. -/// Because this state is a global object and part of the public API it has synchronization primitive using a lock. -/// - Note: The type is safe to use from multiple threads. -final class UnimplementedLoggingState { - struct Description: Equatable { - let typeDescription: String - let function: String - } - - enum MarkingResult { - case alreadyMarked - case marked - } - - private let lock: NSLock = .init() - private var warned: [Description] = [] - - func markWarned(_ description: Description) -> MarkingResult { - lock.lock(); defer { - lock.unlock() - } - guard !warned.contains(description) else { - return .alreadyMarked - } - warned.append(description) - return .marked - } - - func clear() { - lock.lock(); defer { - lock.unlock() - } - warned.removeAll() - } - - func countWarned(forTypeDescription typeDescription: String) -> Int { - return warned - .filter { $0.typeDescription == typeDescription } - .count - } -} - -/// - Note: Exposed as internal to verify the behaviour in tests. -let _unimplementedLoggingState: UnimplementedLoggingState = .init() diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Utils/UserAgent.swift b/ios/Classes/Navigation/MapboxNavigationCore/Utils/UserAgent.swift deleted file mode 100644 index 495816641..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Utils/UserAgent.swift +++ /dev/null @@ -1,100 +0,0 @@ -import Foundation -import MapboxCommon_Private - -extension URLRequest { - public mutating func setNavigationUXUserAgent() { - setValue(.navigationUXUserAgent, forHTTPHeaderField: "User-Agent") - } -} - -extension String { - public static let navigationUXUserAgent: String = { - let processInfo = ProcessInfo() - let systemVersion = processInfo.operatingSystemVersion - let version = [ - systemVersion.majorVersion, - systemVersion.minorVersion, - systemVersion.patchVersion, - ].map(String.init).joined(separator: ".") - let system = processInfo.system() - - let systemComponent = [system, version].joined(separator: "/") - -#if targetEnvironment(simulator) - var simulator: String? = "Simulator" -#else - var simulator: String? -#endif - - let otherComponents = [ - processInfo.chip(), - simulator, - ].compactMap { $0 } - - let mainBundleId = Bundle.main.bundleIdentifier ?? Bundle.main.bundleURL.lastPathComponent - - let components = [ - "\(mainBundleId)/\(Bundle.main.version ?? "unknown")", - navigationUXUserAgentFragment, - systemComponent, - "(\(otherComponents.joined(separator: "; ")))", - ] - let userAgent = components.joined(separator: " ") - Log.info("UserAgent: \(userAgent)", category: .settings) - return userAgent - }() - - public static let navigationUXUserAgentFragment: String = - "\(Bundle.resolvedNavigationSDKName)/\(Bundle.mapboxNavigationVersion)" -} - -extension Bundle { - public static let navigationUXName: String = "mapbox-navigationUX-ios" - public static let navigationUIKitName: String = "mapbox-navigationUIKit-ios" - public static let navigationCoreName: String = "mapbox-navigationCore-ios" - /// Deduced SDK name. - /// - /// Equals ``navigationCoreName``, ``navigationUIKitName`` or ``navigationUXName``, based on the detected project - /// dependencies structure. - public static var resolvedNavigationSDKName: String { - if NSClassFromString("NavigationUX") != nil { - navigationUXName - } else if NSClassFromString("NavigationViewController") != nil { - navigationUIKitName - } else { - navigationCoreName - } - } - - var version: String? { - infoDictionary?["CFBundleShortVersionString"] as? String - } -} - -extension ProcessInfo { - fileprivate func chip() -> String { -#if arch(x86_64) - "x86_64" -#elseif arch(arm) - "arm" -#elseif arch(arm64) - "arm64" -#elseif arch(i386) - "i386" -#endif - } - - fileprivate func system() -> String { -#if os(OSX) - "macOS" -#elseif os(iOS) - "iOS" -#elseif os(watchOS) - "watchOS" -#elseif os(tvOS) - "tvOS" -#elseif os(Linux) - "Linux" -#endif - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/Version.swift b/ios/Classes/Navigation/MapboxNavigationCore/Version.swift deleted file mode 100644 index 25590f1bb..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/Version.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -extension Bundle { - public static let mapboxNavigationVersion: String = "3.5.0" - public static let mapboxNavigationUXBundleIdentifier: String = "com.mapbox.navigationUX" -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerClient.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerClient.swift deleted file mode 100644 index be5ff2907..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerClient.swift +++ /dev/null @@ -1,93 +0,0 @@ -@preconcurrency import AVFoundation - -struct AudioPlayerClient: Sendable { - var play: @Sendable (_ url: URL) async throws -> Bool - var load: @Sendable (_ sounds: [URL]) async throws -> Void -} - -@globalActor actor AudioPlayerActor { - static let shared = AudioPlayerActor() -} - -extension AudioPlayerClient { - @AudioPlayerActor - static func liveValue() -> AudioPlayerClient { - let audioActor = AudioActor() - return Self( - play: { sound in - try await audioActor.play(sound: sound) - }, - load: { sounds in - try await audioActor.load(sounds: sounds) - } - ) - } -} - -private actor AudioActor { - enum Failure: Error { - case soundIsPlaying(URL) - case soundNotLoaded(URL) - case soundsNotLoaded([URL: Error]) - } - - var players: [URL: AVAudioPlayer] = [:] - - func load(sounds: [URL]) throws { - let sounds = sounds.filter { !players.keys.contains($0) } - var errors: [URL: Error] = [:] - for sound in sounds { - do { - let player = try AVAudioPlayer(contentsOf: sound) - players[sound] = player - } catch { - errors[sound] = error - } - } - - guard errors.isEmpty else { - throw Failure.soundsNotLoaded(errors) - } - } - - func play(sound: URL) async throws -> Bool { - guard let player = players[sound] else { - throw Failure.soundNotLoaded(sound) - } - - guard !player.isPlaying else { - throw Failure.soundIsPlaying(sound) - } - - let stream = AsyncThrowingStream { continuation in - let delegate = Delegate(continuation: continuation) - player.delegate = delegate - continuation.onTermination = { _ in - player.stop() - player.currentTime = 0 - _ = delegate - } - - player.play() - } - - return try await stream.first(where: { @Sendable _ in true }) ?? false - } -} - -private final class Delegate: NSObject, AVAudioPlayerDelegate, Sendable { - let continuation: AsyncThrowingStream.Continuation - - init(continuation: AsyncThrowingStream.Continuation) { - self.continuation = continuation - } - - func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { - continuation.yield(flag) - continuation.finish() - } - - func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { - continuation.finish(throwing: error) - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerDelegate.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerDelegate.swift deleted file mode 100644 index f1ea6d4af..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/AudioPlayerDelegate.swift +++ /dev/null @@ -1,11 +0,0 @@ -import AVFAudio - -@MainActor -final class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate { - var onAudioPlayerDidFinishPlaying: (@MainActor (AVAudioPlayer, _ didFinishSuccessfully: Bool) -> Void)? - nonisolated func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { - MainActor.assumingIsolated { - onAudioPlayerDidFinishPlaying?(player, flag) - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MapboxSpeechSynthesizer.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MapboxSpeechSynthesizer.swift deleted file mode 100644 index 8e31e42a5..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MapboxSpeechSynthesizer.swift +++ /dev/null @@ -1,381 +0,0 @@ -import _MapboxNavigationHelpers -import AVFoundation -import Combine -import MapboxDirections - -@MainActor -/// ``SpeechSynthesizing`` implementation, using Mapbox Voice API. Uses pre-caching mechanism for upcoming instructions. -public final class MapboxSpeechSynthesizer: SpeechSynthesizing { - private var _voiceInstructions: PassthroughSubject = .init() - public var voiceInstructions: AnyPublisher { - _voiceInstructions.eraseToAnyPublisher() - } - - // MARK: Speech Configuration - - public var muted: Bool = false { - didSet { - updatePlayerVolume(audioPlayer) - } - } - - private var volumeSubscribtion: AnyCancellable? - public var volume: VolumeMode = .system { - didSet { - guard volume != oldValue else { return } - - switch volume { - case .system: - subscribeToSystemVolume() - case .override(let volume): - volumeSubscribtion = nil - audioPlayer?.volume = volume - } - } - } - - private var currentVolume: Float { - switch volume { - case .system: - return 1.0 - case .override(let volume): - return volume - } - } - - private func subscribeToSystemVolume() { - audioPlayer?.volume = AVAudioSession.sharedInstance().outputVolume - volumeSubscribtion = AVAudioSession.sharedInstance().publisher(for: \.outputVolume).sink { [weak self] volume in - self?.audioPlayer?.volume = volume - } - } - - public var locale: Locale? = Locale.autoupdatingCurrent - - /// Number of upcoming `Instructions` to be pre-fetched. - /// - /// Higher number may exclude cases when required vocalization data is not yet loaded, but also will increase - /// network consumption at the beginning of the route. Keep in mind that pre-fetched instuctions are not guaranteed - /// to be vocalized at all due to re-routing or user actions. "0" will effectively disable pre-fetching. - public var stepsAheadToCache: UInt = 3 - - /// An `AVAudioPlayer` through which spoken instructions are played. - private var audioPlayer: AVAudioPlayer? { - _audioPlayer?.audioPlayer - } - - private var _audioPlayer: SendableAudioPlayer? - private let audioPlayerDelegate: AudioPlayerDelegate = .init() - - /// Controls if this speech synthesizer is allowed to manage the shared `AVAudioSession`. - /// Set this field to `false` if you want to manage the session yourself, for example if your app has background - /// music. - /// Default value is `true`. - public var managesAudioSession: Bool = true - - /// Mapbox speech engine instance. - /// - /// The speech synthesizer uses this object to convert instruction text to audio. - private(set) var remoteSpeechSynthesizer: SpeechSynthesizer - - private var cache: SyncBimodalCache - private var audioTask: Task? - - private var previousInstruction: SpokenInstruction? - - // MARK: Instructions vocalization - - /// Checks if speech synthesizer is now pronouncing an instruction. - public var isSpeaking: Bool { - return audioPlayer?.isPlaying ?? false - } - - /// Creates new `MapboxSpeechSynthesizer` with standard `SpeechSynthesizer` for converting text to audio. - /// - /// - parameter accessToken: A Mapbox [access token](https://www.mapbox.com/help/define-access-token/) used to - /// authorize Mapbox Voice API requests. If an access token is not specified when initializing the speech - /// synthesizer object, it should be specified in the `MBXAccessToken` key in the main application bundle’s - /// Info.plist. - /// - parameter host: An optional hostname to the server API. The Mapbox Voice API endpoint is used by default. - init( - apiConfiguration: ApiConfiguration, - skuTokenProvider: SkuTokenProvider - ) { - self.cache = MapboxSyncBimodalCache() - - self.remoteSpeechSynthesizer = SpeechSynthesizer( - apiConfiguration: apiConfiguration, - skuTokenProvider: skuTokenProvider - ) - - subscribeToSystemVolume() - } - - deinit { - Task { @MainActor [_audioPlayer] in - _audioPlayer?.stop() - } - } - - public func prepareIncomingSpokenInstructions(_ instructions: [SpokenInstruction], locale: Locale? = nil) { - guard let locale = locale ?? self.locale else { - _voiceInstructions.send( - VoiceInstructionEvents.EncounteredError( - error: SpeechError.undefinedSpeechLocale( - instruction: instructions.first! - ) - ) - ) - return - } - - for insturction in instructions.prefix(Int(stepsAheadToCache)) { - if !hasCachedSpokenInstructionForKey(insturction.ssmlText, with: locale) { - downloadAndCacheSpokenInstruction(instruction: insturction, locale: locale) - } - } - } - - public func speak(_ instruction: SpokenInstruction, during legProgress: RouteLegProgress, locale: Locale? = nil) { - guard !muted else { return } - guard let locale = locale ?? self.locale else { - _voiceInstructions.send( - VoiceInstructionEvents.EncounteredError( - error: SpeechError.undefinedSpeechLocale( - instruction: instruction - ) - ) - ) - return - } - - guard let data = cachedDataForKey(instruction.ssmlText, with: locale) else { - fetchAndSpeak(instruction: instruction, locale: locale) - return - } - - _voiceInstructions.send( - VoiceInstructionEvents.WillSpeak( - instruction: instruction - ) - ) - safeDuckAudio(instruction: instruction) - speak(instruction, data: data) - } - - public func stopSpeaking() { - audioPlayer?.stop() - } - - public func interruptSpeaking() { - audioPlayer?.stop() - } - - /// Vocalize the provided audio data. - /// - /// This method is a final part of a vocalization pipeline. It passes audio data to the audio player. `instruction` - /// is used mainly for logging and reference purposes. It's text contents do not affect the vocalization while the - /// actual audio is passed via `data`. - /// - parameter instruction: corresponding instruction to be vocalized. Used for logging and reference. Modifying - /// it's `text` or `ssmlText` does not affect vocalization. - /// - parameter data: audio data, as provided by `remoteSpeechSynthesizer`, to be played. - public func speak(_ instruction: SpokenInstruction, data: Data) { - if let audioPlayer { - if let previousInstruction, audioPlayer.isPlaying { - _voiceInstructions.send( - VoiceInstructionEvents.DidInterrupt( - interruptedInstruction: previousInstruction, - interruptingInstruction: instruction - ) - ) - } - - deinitAudioPlayer() - } - - switch safeInitializeAudioPlayer( - data: data, - instruction: instruction - ) { - case .success(let player): - _audioPlayer = .init(player) - previousInstruction = instruction - audioPlayer?.play() - case .failure(let error): - safeUnduckAudio(instruction: instruction) - _voiceInstructions.send( - VoiceInstructionEvents.EncounteredError( - error: error - ) - ) - } - } - - // MARK: Private Methods - - /// Fetches and plays an instruction. - private func fetchAndSpeak(instruction: SpokenInstruction, locale: Locale) { - audioTask?.cancel() - - _voiceInstructions.send( - VoiceInstructionEvents.WillSpeak( - instruction: instruction - ) - ) - let ssmlText = instruction.ssmlText - let options = SpeechOptions(ssml: ssmlText, locale: locale) - - audioTask = Task { - do { - let audio = try await self.remoteSpeechSynthesizer.audioData(with: options) - try Task.checkCancellation() - self.cache(audio, forKey: ssmlText, with: locale) - self.safeDuckAudio(instruction: instruction) - self.speak( - instruction, - data: audio - ) - } catch let speechError as SpeechErrorApiError { - switch speechError { - case .transportError(underlying: let urlError) where urlError.code == .cancelled: - // Since several voice instructions might be received almost at the same time cancelled - // URLSessionDataTask is not considered as error. - // This means that in this case fallback to another speech synthesizer will not be performed. - break - default: - self._voiceInstructions.send( - VoiceInstructionEvents.EncounteredError( - error: SpeechError.apiError( - instruction: instruction, - options: options, - underlying: speechError - ) - ) - ) - } - } - } - } - - private func downloadAndCacheSpokenInstruction(instruction: SpokenInstruction, locale: Locale) { - let ssmlText = instruction.ssmlText - let options = SpeechOptions(ssml: ssmlText, locale: locale) - - Task { - do { - let audio = try await remoteSpeechSynthesizer.audioData(with: options) - cache(audio, forKey: ssmlText, with: locale) - } catch { - Log.error( - "Couldn't cache spoken instruction '\(instruction)' due to error \(error) ", - category: .navigation - ) - } - } - } - - func safeDuckAudio(instruction: SpokenInstruction?) { - guard managesAudioSession else { return } - if let error = AVAudioSession.sharedInstance().tryDuckAudio() { - _voiceInstructions.send( - VoiceInstructionEvents.EncounteredError( - error: SpeechError.unableToControlAudio( - instruction: instruction, - action: .duck, - underlying: error - ) - ) - ) - } - } - - func safeUnduckAudio(instruction: SpokenInstruction?) { - guard managesAudioSession else { return } - if let error = AVAudioSession.sharedInstance().tryUnduckAudio() { - _voiceInstructions.send( - VoiceInstructionEvents.EncounteredError( - error: SpeechError.unableToControlAudio( - instruction: instruction, - action: .unduck, - underlying: error - ) - ) - ) - } - } - - private func cache(_ data: Data, forKey key: String, with locale: Locale) { - cache.store( - data: data, - key: locale.identifier + key, - mode: [.InMemory, .OnDisk] - ) - } - - private func cachedDataForKey(_ key: String, with locale: Locale) -> Data? { - return cache[locale.identifier + key] - } - - private func hasCachedSpokenInstructionForKey(_ key: String, with locale: Locale) -> Bool { - return cachedDataForKey(key, with: locale) != nil - } - - private func updatePlayerVolume(_ player: AVAudioPlayer?) { - player?.volume = muted ? 0.0 : currentVolume - } - - private func safeInitializeAudioPlayer( - data: Data, - instruction: SpokenInstruction - ) -> Result { - do { - let player = try AVAudioPlayer(data: data) - player.delegate = audioPlayerDelegate - audioPlayerDelegate.onAudioPlayerDidFinishPlaying = { [weak self] _, _ in - guard let self else { return } - safeUnduckAudio(instruction: previousInstruction) - - guard let instruction = previousInstruction else { - assertionFailure("Speech Synthesizer finished speaking 'nil' instruction") - return - } - - _voiceInstructions.send( - VoiceInstructionEvents.DidSpeak( - instruction: instruction - ) - ) - } - updatePlayerVolume(player) - - return .success(player) - } catch { - return .failure(SpeechError.unableToInitializePlayer( - playerType: AVAudioPlayer.self, - instruction: instruction, - synthesizer: remoteSpeechSynthesizer, - underlying: error - )) - } - } - - private func deinitAudioPlayer() { - audioPlayer?.stop() - audioPlayer?.delegate = nil - } -} - -@MainActor -private final class SendableAudioPlayer: Sendable { - let audioPlayer: AVAudioPlayer - - init(_ audioPlayer: AVAudioPlayer) { - self.audioPlayer = audioPlayer - } - - nonisolated func stop() { - DispatchQueue.main.async { - self.audioPlayer.stop() - } - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MultiplexedSpeechSynthesizer.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MultiplexedSpeechSynthesizer.swift deleted file mode 100644 index 97535cfe8..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/MultiplexedSpeechSynthesizer.swift +++ /dev/null @@ -1,181 +0,0 @@ -import _MapboxNavigationHelpers -import AVFoundation -import Combine -import MapboxDirections - -/// ``SpeechSynthesizing``implementation, aggregating other implementations, to allow 'fallback' mechanism. -/// Can be initialized with array of synthesizers which will be called in order of appearance, until one of them is -/// capable to vocalize current ``SpokenInstruction`` -public final class MultiplexedSpeechSynthesizer: SpeechSynthesizing { - private static let mutedDefaultKey = "com.mapbox.navigation.MultiplexedSpeechSynthesizer.isMuted" - private var _voiceInstructions: PassthroughSubject = .init() - public var voiceInstructions: AnyPublisher { - _voiceInstructions.eraseToAnyPublisher() - } - - // MARK: Speech Configuration - - public var muted: Bool = false { - didSet { - applyMute() - } - } - - public var volume: VolumeMode = .system { - didSet { - applyVolume() - } - } - - public var locale: Locale? = Locale.autoupdatingCurrent { - didSet { - applyLocale() - } - } - - private func applyMute() { - UserDefaults.standard.setValue(muted, forKey: Self.mutedDefaultKey) - speechSynthesizers.forEach { $0.muted = muted } - } - - private func applyVolume() { - speechSynthesizers.forEach { $0.volume = volume } - } - - private func applyLocale() { - speechSynthesizers.forEach { $0.locale = locale } - } - - /// Controls if this speech synthesizer is allowed to manage the shared `AVAudioSession`. - /// Set this field to `false` if you want to manage the session yourself, for example if your app has background - /// music. - /// Default value is `true`. - public var managesAudioSession: Bool { - get { - speechSynthesizers.allSatisfy { $0.managesAudioSession == true } - } - set { - speechSynthesizers.forEach { $0.managesAudioSession = newValue } - } - } - - // MARK: Instructions vocalization - - public var isSpeaking: Bool { - return speechSynthesizers.first(where: { $0.isSpeaking }) != nil - } - - private var synthesizersSubscriptions: [AnyCancellable] = [] - public var speechSynthesizers: [any SpeechSynthesizing] { - willSet { - upstreamSynthesizersWillUpdate(newValue) - } - didSet { - upstreamSynthesizersUpdated() - } - } - - private var currentLegProgress: RouteLegProgress? - private var currentInstruction: SpokenInstruction? - - public init(speechSynthesizers: [any SpeechSynthesizing]) { - self.speechSynthesizers = speechSynthesizers - applyVolume() - postInit() - } - - public convenience init( - mapboxSpeechApiConfiguration: ApiConfiguration, - skuTokenProvider: @Sendable @escaping () -> String?, - customSpeechSynthesizers: [SpeechSynthesizing] = [] - ) { - var speechSynthesizers = customSpeechSynthesizers - speechSynthesizers.append(MapboxSpeechSynthesizer( - apiConfiguration: mapboxSpeechApiConfiguration, - skuTokenProvider: .init(skuToken: skuTokenProvider) - )) - speechSynthesizers.append(SystemSpeechSynthesizer()) - self.init(speechSynthesizers: speechSynthesizers) - } - - private func postInit() { - muted = UserDefaults.standard.bool(forKey: Self.mutedDefaultKey) - upstreamSynthesizersWillUpdate(speechSynthesizers) - upstreamSynthesizersUpdated() - } - - public func prepareIncomingSpokenInstructions(_ instructions: [SpokenInstruction], locale: Locale? = nil) { - speechSynthesizers.forEach { $0.prepareIncomingSpokenInstructions(instructions, locale: locale) } - } - - public func speak(_ instruction: SpokenInstruction, during legProgress: RouteLegProgress, locale: Locale? = nil) { - currentLegProgress = legProgress - currentInstruction = instruction - speechSynthesizers.first?.speak(instruction, during: legProgress, locale: locale) - } - - public func stopSpeaking() { - speechSynthesizers.forEach { $0.stopSpeaking() } - } - - public func interruptSpeaking() { - speechSynthesizers.forEach { $0.interruptSpeaking() } - } - - private func upstreamSynthesizersWillUpdate(_ newValue: [any SpeechSynthesizing]) { - var found: [any SpeechSynthesizing] = [] - let duplicate = newValue.first { newSynth in - if found.first(where: { - return $0 === newSynth - }) == nil { - found.append(newSynth) - return false - } - return true - } - - precondition( - duplicate == nil, - "Single `SpeechSynthesizing` object passed to `MultiplexedSpeechSynthesizer` multiple times!" - ) - - speechSynthesizers.forEach { - $0.interruptSpeaking() - } - synthesizersSubscriptions = [] - } - - private func upstreamSynthesizersUpdated() { - synthesizersSubscriptions = speechSynthesizers.enumerated().map { item in - - return item.element.voiceInstructions.sink { [weak self] event in - switch event { - case let errorEvent as VoiceInstructionEvents.EncounteredError: - switch errorEvent.error { - case .unableToControlAudio(instruction: _, action: _, underlying: _): - // do nothing special - break - default: - if let legProgress = self?.currentLegProgress, - let currentInstruction = self?.currentInstruction, - item.offset + 1 < self?.speechSynthesizers.count ?? 0 - { - self?.speechSynthesizers[item.offset + 1].speak( - currentInstruction, - during: legProgress, - locale: self?.locale - ) - return - } - } - default: - break - } - self?._voiceInstructions.send(event) - } - } - applyMute() - applyVolume() - applyLocale() - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/RouteVoiceController.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/RouteVoiceController.swift deleted file mode 100644 index d8651af0a..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/RouteVoiceController.swift +++ /dev/null @@ -1,150 +0,0 @@ -import AVFoundation -import Combine -import Foundation -import UIKit - -@MainActor -public final class RouteVoiceController { - public internal(set) var speechSynthesizer: SpeechSynthesizing - var subscriptions: Set = [] - - /// If true, a noise indicating the user is going to be rerouted will play prior to rerouting. - public var playsRerouteSound: Bool = true - - public init( - routeProgressing: AnyPublisher, - rerouteStarted: AnyPublisher, - fasterRouteSet: AnyPublisher, - speechSynthesizer: SpeechSynthesizing - ) { - self.speechSynthesizer = speechSynthesizer - loadSounds() - - routeProgressing - .sink { [weak self] state in - self?.handle(routeProgressState: state) - } - .store(in: &subscriptions) - - rerouteStarted - .sink { [weak self] in - Task { [weak self] in - await self?.playReroutingSound() - } - } - .store(in: &subscriptions) - - fasterRouteSet - .sink { [weak self] in - Task { [weak self] in - await self?.playReroutingSound() - } - } - .store(in: &subscriptions) - - verifyBackgroundAudio() - } - - private func verifyBackgroundAudio() { - Task { - guard UIApplication.shared.isKind(of: UIApplication.self) else { - return - } - - if !Bundle.main.backgroundModes.contains("audio") { - assertionFailure( - "This application’s Info.plist file must include “audio” in UIBackgroundModes. This background mode is used for spoken instructions while the application is in the background." - ) - } - } - } - - private func handle(routeProgressState: RouteProgressState?) { - guard let routeProgress = routeProgressState?.routeProgress, - let spokenInstruction = routeProgressState?.routeProgress.currentLegProgress.currentStepProgress - .currentSpokenInstruction - else { - return - } - - // AVAudioPlayer is flacky on simulator as of iOS 17.1, this is a workaround for UI tests - guard ProcessInfo.processInfo.environment["isUITest"] == nil else { return } - - let locale = routeProgress.route.speechLocale - - var remainingSpokenInstructions = routeProgressState?.routeProgress.currentLegProgress.currentStepProgress - .remainingSpokenInstructions ?? [] - let nextStepInstructions = routeProgressState?.routeProgress.upcomingLeg?.steps.first? - .instructionsSpokenAlongStep - remainingSpokenInstructions.append(contentsOf: nextStepInstructions ?? []) - if !remainingSpokenInstructions.isEmpty { - speechSynthesizer.prepareIncomingSpokenInstructions( - remainingSpokenInstructions, - locale: locale - ) - } - - speechSynthesizer.locale = locale - speechSynthesizer.speak( - spokenInstruction, - during: routeProgress.currentLegProgress, - locale: locale - ) - } - - private func playReroutingSound() async { - guard playsRerouteSound, !speechSynthesizer.muted else { - return - } - - speechSynthesizer.stopSpeaking() - - guard let rerouteSoundUrl = Bundle.mapboxNavigationUXCore.rerouteSoundUrl else { - return - } - - if let error = AVAudioSession.sharedInstance().tryDuckAudio() { - Log.error("Failed to duck sound for reroute with error: \(error)", category: .navigation) - } - - defer { - if let error = AVAudioSession.sharedInstance().tryUnduckAudio() { - Log.error("Failed to unduck sound for reroute with error: \(error)", category: .navigation) - } - } - - do { - let successful = try await Current.audioPlayerClient.play(rerouteSoundUrl) - if !successful { - Log.error("Failed to play sound for reroute", category: .navigation) - } - } catch { - Log.error("Failed to play sound for reroute with error: \(error)", category: .navigation) - } - } - - private func loadSounds() { - Task { - do { - guard let rerouteSoundUrl = Bundle.mapboxNavigationUXCore.rerouteSoundUrl else { return } - try await Current.audioPlayerClient.load([rerouteSoundUrl]) - } catch { - Log.error("Failed to load sound for reroute", category: .navigation) - } - } - } -} - -extension Bundle { - fileprivate var rerouteSoundUrl: URL? { - guard let rerouteSoundUrl = url( - forResource: "reroute-sound", - withExtension: "pcm" - ) else { - Log.error("Failed to find audio file for reroute", category: .navigation) - return nil - } - - return rerouteSoundUrl - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/Speech.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/Speech.swift deleted file mode 100644 index 5cb5930fa..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/Speech.swift +++ /dev/null @@ -1,201 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation - -/// A `SpeechSynthesizer` object converts text into spoken audio. Unlike `AVSpeechSynthesizer`, a `SpeechSynthesizer` -/// object produces audio by sending an HTTP request to the Mapbox Voice API, which produces more natural-sounding audio -/// in various languages. With a speech synthesizer object, you can asynchronously generate audio data based on the -/// ``SpeechOptions`` object you provide, or you can get the URL used to make this request. -/// -/// Use `AVAudioPlayer` to play the audio that a speech synthesizer object produces. -struct SpeechSynthesizer: Sendable { - private let apiConfiguration: ApiConfiguration - private let skuTokenProvider: SkuTokenProvider - private let urlSession: URLSession - - // MARK: Creating a Speech Object - - init( - apiConfiguration: ApiConfiguration, - skuTokenProvider: SkuTokenProvider, - urlSession: URLSession = .shared - ) { - self.apiConfiguration = apiConfiguration - self.skuTokenProvider = skuTokenProvider - self.urlSession = urlSession - } - - // MARK: Getting Speech - - @discardableResult - /// Asynchronously fetches the audio file. - /// This method retrieves the audio asynchronously over a network connection. If a connection error or server error - /// occurs, details about the error are passed into the given completion handler in lieu of the audio file. - /// - Parameter options: A ``SpeechOptions`` object specifying the requirements for the resulting audio file. - /// - Returns: The audio data. - func audioData(with options: SpeechOptions) async throws -> Data { - try await data(with: url(forSynthesizing: options)) - } - - /// Returns a URL session task for the given URL that will run the given closures on completion or error. - /// - Parameter url: The URL to request. - /// - Returns: ``SpeechErrorApiError`` - private func data( - with url: URL - ) async throws -> Data { - var request = URLRequest(url: url) - request.setNavigationUXUserAgent() - - do { - let (data, response) = try await urlSession.data(for: request) - try validateResponse(response, data: data) - return data - } catch let error as SpeechErrorApiError { - throw error - } catch let urlError as URLError { - throw SpeechErrorApiError.transportError(underlying: urlError) - } catch { - throw SpeechErrorApiError.unknownError(underlying: error) - } - } - - /// The HTTP URL used to fetch audio from the API. - private func url(forSynthesizing options: SpeechOptions) -> URL { - var params = options.params - - params.append(apiConfiguration.accessTokenUrlQueryItem()) - - if let skuToken = skuTokenProvider.skuToken() { - params += [URLQueryItem(name: "sku", value: skuToken)] - } - - let unparameterizedURL = URL(string: options.path, relativeTo: apiConfiguration.endPoint)! - var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! - components.queryItems = params - return components.url! - } - - private func validateResponse(_ response: URLResponse, data: Data) throws { - guard response.mimeType == "application/json" else { return } - - let decoder = JSONDecoder() - let serverErrorResponse = try decoder.decode(ServerErrorResponse.self, from: data) - if serverErrorResponse.code == "Ok" || (serverErrorResponse.code == nil && serverErrorResponse.message == nil) { - return - } - try Self.parserServerError( - response: response, - serverErrorResponse: serverErrorResponse - ) - } - - /// Returns an error that supplements the given underlying error with additional information from the an HTTP - /// response’s body or headers. - static func parserServerError( - response: URLResponse, - serverErrorResponse: ServerErrorResponse - ) throws { - guard let response = response as? HTTPURLResponse else { - throw SpeechErrorApiError.serverError(response, serverErrorResponse) - } - - switch response.statusCode { - case 429: - throw SpeechErrorApiError.rateLimited( - rateLimitInterval: response.rateLimitInterval, - rateLimit: response.rateLimit, - resetTime: response.rateLimitResetTime - ) - default: - throw SpeechErrorApiError.serverError(response, serverErrorResponse) - } - } -} - -enum SpeechErrorApiError: LocalizedError { - case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) - case transportError(underlying: URLError) - case unknownError(underlying: Error) - case serverError(URLResponse, ServerErrorResponse) - - var failureReason: String? { - switch self { - case .transportError(underlying: let urlError): - return urlError.userInfo[NSLocalizedFailureReasonErrorKey] as? String - case .rateLimited(rateLimitInterval: let interval, rateLimit: let limit, _): - let intervalFormatter = DateComponentsFormatter() - intervalFormatter.unitsStyle = .full - guard let interval, let limit else { - return "Too many requests." - } - let formattedInterval = intervalFormatter.string(from: interval) ?? "\(interval) seconds" - let formattedCount = NumberFormatter.localizedString(from: NSNumber(value: limit), number: .decimal) - return "More than \(formattedCount) requests have been made with this access token within a period of \(formattedInterval)." - case .serverError(let response, let serverResponse): - if let serverMessage = serverResponse.message { - return serverMessage - } else if let httpResponse = response as? HTTPURLResponse { - return HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode) - } else if let code = serverResponse.code, - let serverStatusCode = Int(code) - { - return HTTPURLResponse.localizedString(forStatusCode: serverStatusCode) - } else { - return "Server error" - } - case .unknownError(underlying: let error as NSError): - return error.userInfo[NSLocalizedFailureReasonErrorKey] as? String - } - } - - var recoverySuggestion: String? { - switch self { - case .transportError(underlying: let urlError): - return urlError.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String - case .rateLimited(rateLimitInterval: _, rateLimit: _, resetTime: let rolloverTime): - guard let rolloverTime else { - return nil - } - let formattedDate: String = DateFormatter.localizedString( - from: rolloverTime, - dateStyle: .long, - timeStyle: .long - ) - return "Wait until \(formattedDate) before retrying." - case .serverError: - return nil - case .unknownError(underlying: let error as NSError): - return error.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String - } - } -} - -extension HTTPURLResponse { - var rateLimit: UInt? { - guard let limit = allHeaderFields["X-Rate-Limit-Limit"] as? String else { - return nil - } - return UInt(limit) - } - - var rateLimitInterval: TimeInterval? { - guard let interval = allHeaderFields["X-Rate-Limit-Interval"] as? String else { - return nil - } - return TimeInterval(interval) - } - - var rateLimitResetTime: Date? { - guard let resetTime = allHeaderFields["X-Rate-Limit-Reset"] as? String else { - return nil - } - guard let resetTimeNumber = Double(resetTime) else { - return nil - } - return Date(timeIntervalSince1970: resetTimeNumber) - } -} - -struct ServerErrorResponse: Decodable { - let code: String? - let message: String? -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechError.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechError.swift deleted file mode 100644 index 2c03609f5..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechError.swift +++ /dev/null @@ -1,60 +0,0 @@ -import AVKit -import Foundation -import MapboxDirections - -/// The speech-related action that failed. -/// - Seealso: ``SpeechError``. -public enum SpeechFailureAction: String, Sendable { - /// A failure occurred while attempting to mix audio. - case mix - /// A failure occurred while attempting to duck audio. - case duck - /// A failure occurred while attempting to unduck audio. - case unduck - /// A failure occurred while attempting to play audio. - case play -} - -/// A error type returned when encountering errors in the speech engine. -public enum SpeechError: LocalizedError { - /// An error occurred when requesting speech assets from a server API. - /// - Parameters: - /// - instruction: the instruction that failed. - /// - options: the SpeechOptions that were used to make the API request. - /// - underlying: the underlying `Error` returned by the API. - case apiError(instruction: SpokenInstruction, options: SpeechOptions, underlying: Error?) - - /// The speech engine did not fail with the error itself, but did not provide actual data to vocalize. - /// - Parameters: - /// - instruction: the instruction that failed. - /// - options: the SpeechOptions that were used to make the API request. - case noData(instruction: SpokenInstruction, options: SpeechOptions) - - /// The speech engine was unable to perform an action on the system audio service. - /// - Parameters: - /// - instruction: The instruction that failed. - /// - action: a `SpeechFailureAction` that describes the action attempted. - /// - underlying: the `Error` that was optrionally returned by the audio service. - case unableToControlAudio(instruction: SpokenInstruction?, action: SpeechFailureAction, underlying: Error?) - - /// The speech engine was unable to initalize an audio player. - /// - Parameters: - /// - playerType: the type of `AVAudioPlayer` that failed to initalize. - /// - instruction: The instruction that failed. - /// - synthesizer: The speech engine that attempted the initalization. - /// - underlying: the `Error` that was returned by the system audio service. - case unableToInitializePlayer( - playerType: AVAudioPlayer.Type, - instruction: SpokenInstruction, - synthesizer: Sendable?, - underlying: Error - ) - - /// There was no `Locale` provided during processing instruction. - /// - parameter instruction: The instruction that failed. - case undefinedSpeechLocale(instruction: SpokenInstruction) - - /// The speech engine does not support provided locale - /// - parameter locale: Offending locale. - case unsupportedLocale(locale: Locale) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechOptions.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechOptions.swift deleted file mode 100644 index ae5a25d29..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechOptions.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Foundation - -public enum TextType: String, Codable, Sendable, Hashable { - case text - case ssml -} - -public enum AudioFormat: String, Codable, Sendable, Hashable { - case mp3 -} - -public enum SpeechGender: String, Codable, Sendable, Hashable { - case female - case male - case neuter -} - -public struct SpeechOptions: Codable, Sendable, Equatable { - public init( - text: String, - locale: Locale - ) { - self.text = text - self.locale = locale - self.textType = .text - } - - public init( - ssml: String, - locale: Locale - ) { - self.text = ssml - self.locale = locale - self.textType = .ssml - } - - /// `String` to create audiofile for. Can either be plain text or - /// [`SSML`](https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language). - /// - /// If `SSML` is provided, `TextType` must be ``TextType/ssml``. - public var text: String - - /// Type of text to synthesize. - /// - /// `SSML` text must be valid `SSML` for request to work. - public let textType: TextType - - /// Audio format for outputted audio file. - public var outputFormat: AudioFormat = .mp3 - - /// The locale in which the audio is spoken. - /// - /// By default, the user's system locale will be used to decide upon an appropriate voice. - public var locale: Locale - - /// Gender of voice speaking text. - /// - /// - Note: not all languages have male and female voices. - public var speechGender: SpeechGender = .neuter - - /// The path of the request URL, not including the hostname or any parameters. - var path: String { - var characterSet = CharacterSet.urlPathAllowed - characterSet.remove(charactersIn: "/") - return "voice/v1/speak/\(text.addingPercentEncoding(withAllowedCharacters: characterSet)!)" - } - - /// An array of URL parameters to include in the request URL. - var params: [URLQueryItem] { - var params: [URLQueryItem] = [ - URLQueryItem(name: "textType", value: String(describing: textType)), - URLQueryItem(name: "language", value: locale.identifier), - URLQueryItem(name: "outputFormat", value: String(describing: outputFormat)), - ] - - if speechGender != .neuter { - params.append(URLQueryItem(name: "gender", value: String(describing: speechGender))) - } - - return params - } -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechSynthesizing.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechSynthesizing.swift deleted file mode 100644 index 562ab83e7..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SpeechSynthesizing.swift +++ /dev/null @@ -1,91 +0,0 @@ -import Combine -import Foundation -import MapboxDirections - -/// Protocol for implementing speech synthesizer to be used in ``RouteVoiceController``. -@MainActor -public protocol SpeechSynthesizing: AnyObject, Sendable { - var voiceInstructions: AnyPublisher { get } - - /// Controls muting playback of the synthesizer - var muted: Bool { get set } - /// Controls volume of the voice of the synthesizer. - var volume: VolumeMode { get set } - /// Returns `true` if synthesizer is speaking - var isSpeaking: Bool { get } - /// Locale setting to vocalization. This locale will be used as 'default' if no specific locale is passed for - /// vocalizing each individual instruction. - var locale: Locale? { get set } - /// Controls if this speech synthesizer is allowed to manage the shared `AVAudioSession`. - /// Set this field to `false` if you want to manage the session yourself, for example if your app has background - /// music. - /// Default value is `true`. - var managesAudioSession: Bool { get set } - - /// Used to notify speech synthesizer about future spoken instructions in order to give extra time for preparations. - /// - parameter instructions: An array of ``SpokenInstruction``s that will be encountered further. - /// - parameter locale: A locale to be used for preparing instructions. If `nil` is passed - - /// ``SpeechSynthesizing/locale`` will be used as 'default'. - /// - /// It is not guaranteed that all these instructions will be spoken. For example navigation may be re-routed. - /// This method may be (and most likely will be) called multiple times along the route progress - func prepareIncomingSpokenInstructions(_ instructions: [SpokenInstruction], locale: Locale?) - - /// A request to vocalize the instruction - /// - parameter instruction: an instruction to be vocalized - /// - parameter legProgress: current leg progress, corresponding to the instruction - /// - parameter locale: A locale to be used for vocalizing the instruction. If `nil` is passed - - /// ``SpeechSynthesizing/locale`` will be used as 'default'. - /// - /// This method is not guaranteed to be synchronous or asynchronous. When vocalizing is finished, - /// ``VoiceInstructionEvents/DidSpeak`` should be published by ``voiceInstructions``. - func speak(_ instruction: SpokenInstruction, during legProgress: RouteLegProgress, locale: Locale?) - - /// Tells synthesizer to stop current vocalization in a graceful manner. - func stopSpeaking() - /// Tells synthesizer to stop current vocalization immediately. - func interruptSpeaking() -} - -public protocol VoiceInstructionEvent {} - -public enum VoiceInstructionEvents { - public struct WillSpeak: VoiceInstructionEvent, Equatable { - public let instruction: SpokenInstruction - - public init(instruction: SpokenInstruction) { - self.instruction = instruction - } - } - - public struct DidSpeak: VoiceInstructionEvent, Equatable { - public let instruction: SpokenInstruction - - public init(instruction: SpokenInstruction) { - self.instruction = instruction - } - } - - public struct DidInterrupt: VoiceInstructionEvent, Equatable { - public let interruptedInstruction: SpokenInstruction - public let interruptingInstruction: SpokenInstruction - - public init(interruptedInstruction: SpokenInstruction, interruptingInstruction: SpokenInstruction) { - self.interruptedInstruction = interruptedInstruction - self.interruptingInstruction = interruptingInstruction - } - } - - public struct EncounteredError: VoiceInstructionEvent { - public let error: SpeechError - - public init(error: SpeechError) { - self.error = error - } - } -} - -public enum VolumeMode: Equatable, Sendable { - case system - case override(Float) -} diff --git a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SystemSpeechSynthesizer.swift b/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SystemSpeechSynthesizer.swift deleted file mode 100644 index 51b5e7d74..000000000 --- a/ios/Classes/Navigation/MapboxNavigationCore/VoiceGuidance/SystemSpeechSynthesizer.swift +++ /dev/null @@ -1,251 +0,0 @@ -import AVFoundation -import Combine -import MapboxDirections - -/// ``SpeechSynthesizing`` implementation, using ``AVSpeechSynthesizer``. -@_spi(MapboxInternal) -public final class SystemSpeechSynthesizer: NSObject, SpeechSynthesizing { - private let _voiceInstructions: PassthroughSubject = .init() - public var voiceInstructions: AnyPublisher { - _voiceInstructions.eraseToAnyPublisher() - } - - // MARK: Speech Configuration - - public var muted: Bool = false { - didSet { - if isSpeaking { - interruptSpeaking() - } - } - } - - public var volume: VolumeMode { - get { - .system - } - set { - // Do Nothing - // AVSpeechSynthesizer uses 'AVAudioSession.sharedInstance().outputVolume' by default - } - } - - public var locale: Locale? = Locale.autoupdatingCurrent - - /// Controls if this speech synthesizer is allowed to manage the shared `AVAudioSession`. - /// Set this field to `false` if you want to manage the session yourself, for example if your app has background - /// music. - /// Default value is `true`. - public var managesAudioSession = true - - // MARK: Speaking Instructions - - public var isSpeaking: Bool { return speechSynthesizer.isSpeaking } - - private var speechSynthesizer: AVSpeechSynthesizer { - _speechSynthesizer.speechSynthesizer - } - - /// Holds `AVSpeechSynthesizer` that can be sent between isolation contexts but should be operated on MainActor. - /// - /// Motivation: - /// We must stop synthesizer when the instance is deallocated, but deinit isn't guaranteed to be called on - /// MainActor. So we can't safely access synthesizer from it. - private var _speechSynthesizer: SendableSpeechSynthesizer - - private var previousInstruction: SpokenInstruction? - - override public init() { - self._speechSynthesizer = .init(AVSpeechSynthesizer()) - super.init() - speechSynthesizer.delegate = self - } - - deinit { - Task { @MainActor [_speechSynthesizer] in - _speechSynthesizer.speechSynthesizer.stopSpeaking(at: .immediate) - } - } - - public func prepareIncomingSpokenInstructions(_ instructions: [SpokenInstruction], locale: Locale?) { - // Do nothing - } - - public func speak(_ instruction: SpokenInstruction, during legProgress: RouteLegProgress, locale: Locale? = nil) { - guard !muted else { - _voiceInstructions.send( - VoiceInstructionEvents.DidSpeak( - instruction: instruction - ) - ) - return - } - - guard let locale = locale ?? self.locale else { - _voiceInstructions.send( - VoiceInstructionEvents.EncounteredError( - error: SpeechError.undefinedSpeechLocale( - instruction: instruction - ) - ) - ) - return - } - - var utterance: AVSpeechUtterance? - let localeCode = [locale.languageCode, locale.regionCode].compactMap { $0 }.joined(separator: "-") - - if localeCode == "en-US" { - // Alex can’t handle attributed text. - utterance = AVSpeechUtterance(string: instruction.text) - utterance!.voice = AVSpeechSynthesisVoice(identifier: AVSpeechSynthesisVoiceIdentifierAlex) - } - - _voiceInstructions.send(VoiceInstructionEvents.WillSpeak(instruction: instruction)) - - if utterance?.voice == nil { - utterance = AVSpeechUtterance(attributedString: instruction.attributedText(for: legProgress)) - } - - // Only localized languages will have a proper fallback voice - if utterance?.voice == nil { - utterance?.voice = AVSpeechSynthesisVoice(language: localeCode) - } - - guard let utteranceToSpeak = utterance else { - _voiceInstructions.send( - VoiceInstructionEvents.EncounteredError( - error: SpeechError.unsupportedLocale( - locale: Locale.nationalizedCurrent - ) - ) - ) - return - } - if let previousInstruction, speechSynthesizer.isSpeaking { - _voiceInstructions.send( - VoiceInstructionEvents.DidInterrupt( - interruptedInstruction: previousInstruction, - interruptingInstruction: instruction - ) - ) - } - - previousInstruction = instruction - speechSynthesizer.speak(utteranceToSpeak) - } - - public func stopSpeaking() { - speechSynthesizer.stopSpeaking(at: .word) - } - - public func interruptSpeaking() { - speechSynthesizer.stopSpeaking(at: .immediate) - } - - private func safeDuckAudio() { - guard managesAudioSession else { return } - if let error = AVAudioSession.sharedInstance().tryDuckAudio() { - guard let instruction = previousInstruction else { - assertionFailure("Speech Synthesizer finished speaking 'nil' instruction") - return - } - - _voiceInstructions.send( - VoiceInstructionEvents.EncounteredError( - error: SpeechError.unableToControlAudio( - instruction: instruction, - action: .duck, - underlying: error - ) - ) - ) - } - } - - private func safeUnduckAudio() { - guard managesAudioSession else { return } - if let error = AVAudioSession.sharedInstance().tryUnduckAudio() { - guard let instruction = previousInstruction else { - assertionFailure("Speech Synthesizer finished speaking 'nil' instruction") - return - } - - _voiceInstructions.send( - VoiceInstructionEvents.EncounteredError( - error: SpeechError.unableToControlAudio( - instruction: instruction, - action: .unduck, - underlying: error - ) - ) - ) - } - } -} - -extension SystemSpeechSynthesizer: AVSpeechSynthesizerDelegate { - public nonisolated func speechSynthesizer( - _ synthesizer: AVSpeechSynthesizer, - didStart utterance: AVSpeechUtterance - ) { - MainActor.assumingIsolated { - safeDuckAudio() - } - } - - public nonisolated func speechSynthesizer( - _ synthesizer: AVSpeechSynthesizer, - didContinue utterance: AVSpeechUtterance - ) { - MainActor.assumingIsolated { - safeDuckAudio() - } - } - - public nonisolated func speechSynthesizer( - _ synthesizer: AVSpeechSynthesizer, - didFinish utterance: AVSpeechUtterance - ) { - MainActor.assumingIsolated { - safeUnduckAudio() - guard let instruction = previousInstruction else { - assertionFailure("Speech Synthesizer finished speaking 'nil' instruction") - return - } - _voiceInstructions.send(VoiceInstructionEvents.DidSpeak(instruction: instruction)) - } - } - - public nonisolated func speechSynthesizer( - _ synthesizer: AVSpeechSynthesizer, - didPause utterance: AVSpeechUtterance - ) { - MainActor.assumingIsolated { - safeUnduckAudio() - } - } - - public nonisolated func speechSynthesizer( - _ synthesizer: AVSpeechSynthesizer, - didCancel utterance: AVSpeechUtterance - ) { - MainActor.assumingIsolated { - safeUnduckAudio() - guard let instruction = previousInstruction else { - assertionFailure("Speech Synthesizer finished speaking 'nil' instruction") - return - } - _voiceInstructions.send(VoiceInstructionEvents.DidSpeak(instruction: instruction)) - } - } -} - -@MainActor -private final class SendableSpeechSynthesizer: Sendable { - let speechSynthesizer: AVSpeechSynthesizer - - init(_ speechSynthesizer: AVSpeechSynthesizer) { - self.speechSynthesizer = speechSynthesizer - } -} diff --git a/ios/MapboxDirections.podspec b/ios/MapboxDirections.podspec deleted file mode 100644 index 8ef9ac37b..000000000 --- a/ios/MapboxDirections.podspec +++ /dev/null @@ -1,28 +0,0 @@ -# -# 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 |md| - md.name = 'MapboxDirections' - md.version = '3.5.0' - md.summary = 'Mapbox Directions.' - md.author = { 'Mapbox' => 'mobile@mapbox.com' } - md.source = { :git => "https://github.com/mapbox/mapbox-navigation-ios.git", :tag => "v3.5.0" } - - md.homepage = 'https://github.com/mapbox/mapbox-navigation-ios' - md.license = 'MIT' - md.author = { 'Mapbox' => 'mobile@mapbox.com' } - - md.source_files = 'Sources/MapboxDirections/**/*.{h,m,swift}' - - md.requires_arc = true - md.module_name = "MapboxDirections" - - md.platform = :ios, '13.0' - - md.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - md.swift_version = '5.8' - - md.dependency 'Turf', '3.0.0' -end \ No newline at end of file diff --git a/ios/MapboxNavigationCore.podspec b/ios/MapboxNavigationCore.podspec deleted file mode 100644 index 0cdbe834d..000000000 --- a/ios/MapboxNavigationCore.podspec +++ /dev/null @@ -1,33 +0,0 @@ -# -# 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 |nav| - nav.name = 'MapboxNavigationCore' - nav.version = '3.5.0' - nav.summary = 'Mapbox Navigation SDK.' - nav.author = { 'Mapbox' => 'mobile@mapbox.com' } - nav.source = { :git => "https://github.com/mapbox/mapbox-navigation-ios.git", :tag => "v3.5.0" } - - nav.homepage = 'https://github.com/mapbox/mapbox-navigation-ios' - nav.license = 'MIT' - nav.author = { 'Mapbox' => 'mobile@mapbox.com' } - - nav.source_files = 'Sources/MapboxNavigationCore/**/*.{h,m,swift}' - nav.resource_bundle = { 'MapboxCoreNavigationResources' => ['Sources/MapboxCoreNavigation/Resources/*/*', 'Sources/MapboxCoreNavigation/Resources/*'] } - - nav.requires_arc = true - nav.module_name = "MapboxCoreNavigation" - - nav.platform = :ios, '13.0' - - nav.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - nav.swift_version = '5.8' - - nav.dependency 'Turf', '3.0.0' - #nav.dependency 'MapboxDirections', '3.5.0' - - nav.subspec 'MapboxDirections' do |directions| - end -end \ No newline at end of file diff --git a/ios/Turf.podspec b/ios/Turf.podspec deleted file mode 100644 index b5c9933c8..000000000 --- a/ios/Turf.podspec +++ /dev/null @@ -1,45 +0,0 @@ -Pod::Spec.new do |s| - - # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - - s.name = "Turf" - s.version = "3.0.0" - s.summary = "Simple spatial analysis." - s.description = "A spatial analysis library written in Swift for native iOS, macOS, tvOS, watchOS, visionOS, and Linux applications, ported from Turf.js." - - s.homepage = "https://github.com/mapbox/turf-swift" - - # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - - s.license = { :type => "ISC", :file => "LICENSE.md" } - - # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - - s.author = { "Mapbox" => "mobile@mapbox.com" } - s.social_media_url = "https://twitter.com/mapbox" - - # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - - s.ios.deployment_target = "11.0" - s.osx.deployment_target = "10.13" - s.tvos.deployment_target = "11.0" - s.watchos.deployment_target = "4.0" - # CocoaPods doesn't support releasing of visionOS pods yet, need to wait for v1.15.0 release of CocoaPods - # with this fix https://github.com/CocoaPods/CocoaPods/pull/12159. - # s.visionos.deployment_target = "1.0" - - # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - - s.source = { - :http => "https://github.com/mapbox/turf-swift/releases/download/v#{s.version}/Turf.xcframework.zip" - } - - # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - - s.requires_arc = true - s.module_name = "Turf" - s.frameworks = 'CoreLocation' - s.swift_version = "5.7" - s.vendored_frameworks = 'Turf.xcframework' - -end \ No newline at end of file diff --git a/ios/mapbox_maps_flutter.podspec b/ios/mapbox_maps_flutter.podspec index 4fdc3fe5e..9a834b95c 100644 --- a/ios/mapbox_maps_flutter.podspec +++ b/ios/mapbox_maps_flutter.podspec @@ -7,59 +7,6 @@ # Run `pod lib lint mapbox_maps_flutter.podspec` to validate before publishing. # -Pod::Spec.new do |md| - md.name = 'MapboxDirections' - md.version = '3.5.0' - md.summary = 'Mapbox Directions.' - md.author = { 'Mapbox' => 'mobile@mapbox.com' } - md.source = { :git => "https://github.com/mapbox/mapbox-navigation-ios.git", :tag => "v3.5.0" } - - md.homepage = 'https://github.com/mapbox/mapbox-navigation-ios' - md.license = 'MIT' - md.author = { 'Mapbox' => 'mobile@mapbox.com' } - - md.source_files = 'Sources/MapboxDirections/**/*.{h,m,swift}' - - md.requires_arc = true - md.module_name = "MapboxDirections" - - md.platform = :ios, '13.0' - - md.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - md.swift_version = '5.8' - - md.dependency 'Turf', '3.0.0' -end - -Pod::Spec.new do |nav| - nav.name = 'MapboxNavigationCore' - nav.version = '3.5.0' - nav.summary = 'Mapbox Navigation SDK.' - nav.author = { 'Mapbox' => 'mobile@mapbox.com' } - nav.source = { :git => "https://github.com/mapbox/mapbox-navigation-ios.git", :tag => "v3.5.0" } - - nav.homepage = 'https://github.com/mapbox/mapbox-navigation-ios' - nav.license = 'MIT' - nav.author = { 'Mapbox' => 'mobile@mapbox.com' } - - nav.source_files = 'Sources/MapboxNavigationCore/**/*.{h,m,swift}' - nav.resource_bundle = { 'MapboxCoreNavigationResources' => ['Sources/MapboxCoreNavigation/Resources/*/*', 'Sources/MapboxCoreNavigation/Resources/*'] } - - nav.requires_arc = true - nav.module_name = "MapboxCoreNavigation" - - nav.platform = :ios, '13.0' - - nav.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - nav.swift_version = '5.8' - - nav.dependency 'Turf', '3.0.0' - #nav.dependency 'MapboxDirections', '3.5.0' - - nav.subspec 'MapboxDirections' do |directions| - end -end - Pod::Spec.new do |s| s.name = 'mapbox_maps_flutter' s.version = '2.4.0' @@ -73,7 +20,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '13.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' } @@ -82,9 +29,5 @@ Pod::Spec.new do |s| s.dependency 'MapboxMaps', '11.8.0' s.dependency 'Turf', '3.0.0' - s.subspec 'MapboxDirections' do |directions| - end - - s.subspec 'MapboxNavigationCore' do |nav| - end + s.dependency 'MapboxNavigationCoreUnofficial', '3.5.0' end diff --git a/mapbox-maps-flutter b/mapbox-maps-flutter new file mode 160000 index 000000000..e298bbaa8 --- /dev/null +++ b/mapbox-maps-flutter @@ -0,0 +1 @@ +Subproject commit e298bbaa88bfee5d0478065268673590475a79e5 From 5b014649e6d810e7fac8b405cab955186d8111c0 Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sun, 26 Jan 2025 15:23:05 +0100 Subject: [PATCH 14/33] fixed compile error, except one --- .../Generated/NavigationMessager.swift | 48 ++++++++-------- ios/Classes/MapboxMapController.swift | 5 +- ios/Classes/NavigationController.swift | 56 +++++++++++-------- 3 files changed, 60 insertions(+), 49 deletions(-) diff --git a/ios/Classes/Generated/NavigationMessager.swift b/ios/Classes/Generated/NavigationMessager.swift index 7d36f7b39..d0dfdfdc0 100644 --- a/ios/Classes/Generated/NavigationMessager.swift +++ b/ios/Classes/Generated/NavigationMessager.swift @@ -35,7 +35,7 @@ private func wrapResult(_ result: Any?) -> [Any?] { } private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { + if let pigeonError = error as? NavigationMessagerError { return [ pigeonError.code, pigeonError.message, @@ -57,7 +57,7 @@ private func wrapError(_ error: Any) -> [Any?] { } private func createConnectionError(withChannelName channelName: String) -> NavigationMessagerError { - return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") + return NavigationMessagerError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") } private func isNullish(_ value: Any?) -> Bool { @@ -421,13 +421,13 @@ class NavigationMessagerPigeonCodec: FlutterStandardMessageCodec, @unchecked Sen /// 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) + 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 @@ -439,7 +439,7 @@ class NavigationListener: NavigationListenerProtocol { var codec: NavigationMessagerPigeonCodec { return NavigationMessagerPigeonCodec.shared } - func onNavigationRouteReady(completion: @escaping (Result) -> Void) { + 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 @@ -451,13 +451,13 @@ class NavigationListener: NavigationListenerProtocol { let code: String = listResponse[0] as! String let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) } else { completion(.success(Void())) } } } - func onNavigationRouteFailed(completion: @escaping (Result) -> 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 @@ -469,13 +469,13 @@ class NavigationListener: NavigationListenerProtocol { let code: String = listResponse[0] as! String let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) } else { completion(.success(Void())) } } } - func onNavigationRouteCancelled(completion: @escaping (Result) -> 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 @@ -487,13 +487,13 @@ class NavigationListener: NavigationListenerProtocol { let code: String = listResponse[0] as! String let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) } else { completion(.success(Void())) } } } - func onNavigationRouteRendered(completion: @escaping (Result) -> 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 @@ -505,13 +505,13 @@ class NavigationListener: NavigationListenerProtocol { let code: String = listResponse[0] as! String let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) } else { completion(.success(Void())) } } } - func onNewLocation(location locationArg: NavigationLocation, completion: @escaping (Result) -> 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 @@ -523,13 +523,13 @@ class NavigationListener: NavigationListenerProtocol { let code: String = listResponse[0] as! String let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) } else { completion(.success(Void())) } } } - func onRouteProgress(routeProgress routeProgressArg: RouteProgress, completion: @escaping (Result) -> 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 @@ -541,13 +541,13 @@ class NavigationListener: NavigationListenerProtocol { let code: String = listResponse[0] as! String let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) } else { completion(.success(Void())) } } } - func onNavigationCameraStateChanged(state stateArg: NavigationCameraState, completion: @escaping (Result) -> 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 @@ -559,7 +559,7 @@ class NavigationListener: NavigationListenerProtocol { let code: String = listResponse[0] as! String let message: String? = nilOrValue(listResponse[1]) let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) } else { completion(.success(Void())) } @@ -568,7 +568,7 @@ class NavigationListener: NavigationListenerProtocol { } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol NavigationInterface { - func setRoute(waypoints: [Point], completion: @escaping (Result) -> Void) + func setRoute(waypoints: [Point], completion: @escaping (Result) -> Void) func stopTripSession(completion: @escaping (Result) -> Void) func startTripSession(withForegroundService: Bool, completion: @escaping (Result) -> Void) func requestNavigationCameraToFollowing(completion: @escaping (Result) -> Void) diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index f8a8ba92d..12a52f7f0 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -7,6 +7,7 @@ struct SuffixBinaryMessenger { let suffix: String } +@MainActor final class MapboxMapController: NSObject, FlutterPlatformView { private let mapView: MapView private let mapboxMap: MapboxMap @@ -84,7 +85,7 @@ final class MapboxMapController: NSObject, FlutterPlatformView { annotationController!.setup(binaryMessenger: binaryMessenger) navigationController = NavigationController(withMapView: mapView) - NavigationInterfaceSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: attributionController, messageChannelSuffix: binaryMessenger.suffix) + NavigationInterfaceSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: navigationController, messageChannelSuffix: binaryMessenger.suffix) super.init() @@ -114,7 +115,7 @@ final class MapboxMapController: NSObject, FlutterPlatformView { result(FlutterError(code: "2342345", message: error.localizedDescription, details: nil)) } case "navigation#add_listeners": - navigationController!.addListeners(messenger: messenger) + navigationController!.addListeners(messenger: binaryMessenger) result(nil) case "navigation#remove_listeners": navigationController!.removeListeners() diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index 8d2951c3d..8523b23dc 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -1,8 +1,10 @@ import Combine import CoreLocation import MapboxDirections -import MapboxCoreNavigation +import MapboxNavigationCore +import MapboxMaps +@MainActor final class NavigationController: NSObject, NavigationInterface { let predictiveCacheManager: PredictiveCacheManager? @@ -11,7 +13,7 @@ final class NavigationController: NSObject, NavigationInterface { @Published private(set) var activeNavigationRoutes: NavigationRoutes? @Published private(set) var routeProgress: RouteProgress? @Published private(set) var currentLocation: CLLocation? - @Published var cameraState: NavigationCameraState = .idle + @Published var cameraState: NavigationCameraState = .iDLE @Published var profileIdentifier: ProfileIdentifier = .automobileAvoidingTraffic @Published var shouldRequestMapMatching = false @@ -21,6 +23,7 @@ final class NavigationController: NSObject, NavigationInterface { private var cancelables: Set = [] private var onNavigationListener: NavigationListener? private let mapView: MapView + private let navigationProvider: MapboxNavigationProvider init(withMapView mapView: MapView) { self.mapView = mapView @@ -29,14 +32,14 @@ final class NavigationController: NSObject, NavigationInterface { credentials: .init(), // You can pass a custom token if you need to, locationSource: .live ) - let navigationProvider = MapboxNavigationProvider(coreConfig: config) - self.core = navigationProvider.mapboxNavigation - self.predictiveCacheManager = navigationProvider.predictiveCacheManager + self.navigationProvider = MapboxNavigationProvider(coreConfig: config) + self.core = self.navigationProvider.mapboxNavigation + self.predictiveCacheManager = self.navigationProvider.predictiveCacheManager self.observeNavigation() } private func observeNavigation() { - core.tripSession().session + self.core.tripSession().session .map { if case .activeGuidance = $0.state { return true } return false @@ -63,43 +66,43 @@ final class NavigationController: NSObject, NavigationInterface { func cancelPreview() { waypoints = [] currentPreviewRoutes = nil - cameraState = .following + cameraState = .fOLLOWING } func startActiveNavigation() { guard let previewRoutes = currentPreviewRoutes else { return } core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) - cameraState = .following + cameraState = .fOLLOWING currentPreviewRoutes = nil waypoints = [] } func stopActiveNavigation() { core.tripSession().startFreeDrive() - cameraState = .following + cameraState = .fOLLOWING } func requestRoutes(points: [Point]) async throws { - waypoints.append(Waypoint(coordinate: mapPoint.coordinate, name: mapPoint.name)) + waypoints.append(Waypoint(coordinate: self.currentLocation!.coordinate, name: "Current location")) let provider = core.routingProvider() if shouldRequestMapMatching { let mapMatchingOptions = NavigationMatchOptions( - waypoints: optionsWaypoints, + waypoints: waypoints, profileIdentifier: profileIdentifier ) let previewRoutes = try await provider.calculateRoutes(options: mapMatchingOptions).value currentPreviewRoutes = previewRoutes } else { let routeOptions = NavigationRouteOptions( - waypoints: optionsWaypoints, + waypoints: waypoints, profileIdentifier: profileIdentifier ) let previewRoutes = try await provider.calculateRoutes(options: routeOptions).value currentPreviewRoutes = previewRoutes } - cameraState = .idle + cameraState = .iDLE } func addListeners(messenger: SuffixBinaryMessenger) { @@ -112,33 +115,40 @@ final class NavigationController: NSObject, NavigationInterface { } func setRoute(waypoints: [Point], completion: @escaping (Result) -> Void) { - self.requestRoutes(waypoints) - completion(.success()) + Task { + do { + try await self.requestRoutes(points: waypoints) + completion(.success(<#T##Void#>)) + } + catch { + completion(.failure(error)) + } + } } func stopTripSession(completion: @escaping (Result) -> Void) { //core.cameraState - cameraState = .overview - completion(.success()) + cameraState = .oVERVIEW + completion(.success(<#T##Void#>)) } func startTripSession(withForegroundService: Bool, completion: @escaping (Result) -> Void) { guard let previewRoutes = currentPreviewRoutes else { return } core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) - cameraState = .following + cameraState = .fOLLOWING currentPreviewRoutes = nil waypoints = [] - completion(.success()) + completion(.success(<#T##Void#>)) } func requestNavigationCameraToFollowing(completion: @escaping (Result) -> Void) { - cameraState = .following - completion(.success()) + cameraState = .fOLLOWING + completion(.success(<#T##Void#>)) } func requestNavigationCameraToOverview(completion: @escaping (Result) -> Void) { - cameraState = .overview - completion(.success()) + cameraState = .oVERVIEW + completion(.success(<#T##Void#>)) } func lastLocation(completion: @escaping (Result) -> Void) { From db924621d1ca4045326ea3136c859ce1be6c3380 Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sun, 26 Jan 2025 15:25:49 +0100 Subject: [PATCH 15/33] delete link --- mapbox-maps-flutter | 1 - 1 file changed, 1 deletion(-) delete mode 160000 mapbox-maps-flutter diff --git a/mapbox-maps-flutter b/mapbox-maps-flutter deleted file mode 160000 index e298bbaa8..000000000 --- a/mapbox-maps-flutter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e298bbaa88bfee5d0478065268673590475a79e5 From b9cc1d1961d2a61632d2317a78353f2195abe42a Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sat, 1 Feb 2025 19:31:21 +0100 Subject: [PATCH 16/33] flt mapping --- example/ios/Runner.xcodeproj/project.pbxproj | 6 +-- ios/Classes/Extensions.swift | 48 +++++++++++++++++++ ios/Classes/MapboxMapFactory.swift | 1 + ios/Classes/MapboxMapsPlugin.swift | 1 + ios/Classes/NavigationController.swift | 49 +++++++++++++------- 5 files changed, 84 insertions(+), 21 deletions(-) diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index deb8905ed..0e572f432 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -553,7 +553,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -634,7 +634,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -683,7 +683,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Classes/Extensions.swift b/ios/Classes/Extensions.swift index 6fe760a8e..6860210f0 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,49 @@ 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( + 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/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 index 8523b23dc..ce095397d 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -9,9 +9,9 @@ final class NavigationController: NSObject, NavigationInterface { let predictiveCacheManager: PredictiveCacheManager? @Published private(set) var isInActiveNavigation: Bool = false - @Published private(set) var currentPreviewRoutes: NavigationRoutes? - @Published private(set) var activeNavigationRoutes: NavigationRoutes? - @Published private(set) var routeProgress: RouteProgress? + @Published private(set) var currentPreviewRoutes: MapboxNavigationCore.NavigationRoutes? + @Published private(set) var activeNavigationRoutes: MapboxNavigationCore.NavigationRoutes? + @Published private(set) var routeProgress: MapboxNavigationCore.RouteProgress? @Published private(set) var currentLocation: CLLocation? @Published var cameraState: NavigationCameraState = .iDLE @Published var profileIdentifier: ProfileIdentifier = .automobileAvoidingTraffic @@ -35,7 +35,10 @@ final class NavigationController: NSObject, NavigationInterface { self.navigationProvider = MapboxNavigationProvider(coreConfig: config) self.core = self.navigationProvider.mapboxNavigation self.predictiveCacheManager = self.navigationProvider.predictiveCacheManager - self.observeNavigation() + //self.observeNavigation() + +// func onRouteProgress(routeProgress routeProgressArg: RouteProgress, completion: @escaping (Result) -> Void) +// func onNavigationCameraStateChanged(state stateArg: NavigationCameraState, completion: @escaping (Result) -> Void) } private func observeNavigation() { @@ -46,17 +49,21 @@ final class NavigationController: NSObject, NavigationInterface { } .removeDuplicates() .assign(to: &$isInActiveNavigation) - - core.navigation().routeProgress - .map { $0?.routeProgress } - .assign(to: &$routeProgress) + + core.navigation().routeProgress.sink { state in + self.routeProgress=state?.routeProgress + if (self.routeProgress != nil) { + self.onNavigationListener?.onRouteProgress(routeProgress: self.routeProgress!.toFLTRouteProgress()) { _ in } + } + } core.tripSession().navigationRoutes .assign(to: &$activeNavigationRoutes) - - core.navigation().locationMatching - .map { $0.enhancedLocation } - .assign(to: &$currentLocation) + + core.navigation().locationMatching.sink { state in + self.currentLocation = state.enhancedLocation + self.onNavigationListener?.onNewLocation(location: state.enhancedLocation.toFLTNavigationLocation()) { _ in } + } } func startFreeDrive() { @@ -94,6 +101,7 @@ final class NavigationController: NSObject, NavigationInterface { ) let previewRoutes = try await provider.calculateRoutes(options: mapMatchingOptions).value currentPreviewRoutes = previewRoutes + self.onNavigationListener?.onNavigationRouteReady() { _ in } } else { let routeOptions = NavigationRouteOptions( waypoints: waypoints, @@ -101,6 +109,7 @@ final class NavigationController: NSObject, NavigationInterface { ) let previewRoutes = try await provider.calculateRoutes(options: routeOptions).value currentPreviewRoutes = previewRoutes + self.onNavigationListener?.onNavigationRouteReady() { _ in } } cameraState = .iDLE } @@ -118,7 +127,7 @@ final class NavigationController: NSObject, NavigationInterface { Task { do { try await self.requestRoutes(points: waypoints) - completion(.success(<#T##Void#>)) + completion(.success(Void())) } catch { completion(.failure(error)) @@ -127,9 +136,10 @@ final class NavigationController: NSObject, NavigationInterface { } func stopTripSession(completion: @escaping (Result) -> Void) { - //core.cameraState + cameraState = .oVERVIEW - completion(.success(<#T##Void#>)) + self.onNavigationListener?.onNavigationCameraStateChanged(state: cameraState) {_ in } + completion(.success(Void())) } func startTripSession(withForegroundService: Bool, completion: @escaping (Result) -> Void) { @@ -138,17 +148,20 @@ final class NavigationController: NSObject, NavigationInterface { cameraState = .fOLLOWING currentPreviewRoutes = nil waypoints = [] - completion(.success(<#T##Void#>)) + self.onNavigationListener?.onNavigationCameraStateChanged(state: cameraState) {_ in } + completion(.success(Void())) } func requestNavigationCameraToFollowing(completion: @escaping (Result) -> Void) { cameraState = .fOLLOWING - completion(.success(<#T##Void#>)) + self.onNavigationListener?.onNavigationCameraStateChanged(state: cameraState) {_ in } + completion(.success(Void())) } func requestNavigationCameraToOverview(completion: @escaping (Result) -> Void) { cameraState = .oVERVIEW - completion(.success(<#T##Void#>)) + self.onNavigationListener?.onNavigationCameraStateChanged(state: cameraState) {_ in } + completion(.success(Void())) } func lastLocation(completion: @escaping (Result) -> Void) { From b71e1ac85a38277d632d765540d4a8fce105924c Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Fri, 7 Feb 2025 20:51:34 +0100 Subject: [PATCH 17/33] static navigation provider --- ios/Classes/MapboxMapController.swift | 9 ++++- ios/Classes/NavigationController.swift | 55 ++++++++++++++++++++------ 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index 12a52f7f0..8be8c10e1 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -1,6 +1,7 @@ import Flutter @_spi(Experimental) import MapboxMaps import UIKit +import MapboxNavigationCore struct SuffixBinaryMessenger { let messenger: FlutterBinaryMessenger @@ -17,6 +18,10 @@ final class MapboxMapController: NSObject, FlutterPlatformView { private let navigationController: NavigationController? private let eventHandler: MapboxEventHandler private let binaryMessenger: SuffixBinaryMessenger + private static let navigationProvider: MapboxNavigationProvider = MapboxNavigationProvider(coreConfig: CoreConfig( + credentials: .init(), // You can pass a custom token if you need to, + locationSource: .live + )) func view() -> UIView { return mapView @@ -48,6 +53,8 @@ final class MapboxMapController: NSObject, FlutterPlatformView { channelSuffix: String(channelSuffix) ) + //mapView.location.override(locationProvider: self.navigationProvider, headingProvider: nil) + let styleController = StyleController(styleManager: mapboxMap) StyleManagerSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: styleController, messageChannelSuffix: binaryMessenger.suffix) @@ -84,7 +91,7 @@ final class MapboxMapController: NSObject, FlutterPlatformView { annotationController = AnnotationController(withMapView: mapView) annotationController!.setup(binaryMessenger: binaryMessenger) - navigationController = NavigationController(withMapView: mapView) + navigationController = NavigationController(withMapView: mapView, navigationProvider: MapboxMapController.navigationProvider ) NavigationInterfaceSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: navigationController, messageChannelSuffix: binaryMessenger.suffix) super.init() diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index ce095397d..69d47fca2 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -25,20 +25,18 @@ final class NavigationController: NSObject, NavigationInterface { private let mapView: MapView private let navigationProvider: MapboxNavigationProvider - init(withMapView mapView: MapView) { + init(withMapView mapView: MapView, navigationProvider: MapboxNavigationProvider) { + self.mapView = mapView - - let config = CoreConfig( - credentials: .init(), // You can pass a custom token if you need to, - locationSource: .live - ) - self.navigationProvider = MapboxNavigationProvider(coreConfig: config) + + self.navigationProvider = navigationProvider + self.core = self.navigationProvider.mapboxNavigation self.predictiveCacheManager = self.navigationProvider.predictiveCacheManager - //self.observeNavigation() -// func onRouteProgress(routeProgress routeProgressArg: RouteProgress, completion: @escaping (Result) -> Void) -// func onNavigationCameraStateChanged(state stateArg: NavigationCameraState, completion: @escaping (Result) -> Void) + super.init() + + observeNavigation() } private func observeNavigation() { @@ -64,6 +62,14 @@ final class NavigationController: NSObject, NavigationInterface { self.currentLocation = state.enhancedLocation self.onNavigationListener?.onNewLocation(location: state.enhancedLocation.toFLTNavigationLocation()) { _ in } } + + if (core.navigation().currentLocationMatching != nil) { + self.currentLocation = self.core.navigation().currentLocationMatching?.enhancedLocation + if(self.currentLocation != nil) + { + self.onNavigationListener?.onNewLocation(location: self.currentLocation!.toFLTNavigationLocation()) { _ in } + } + } } func startFreeDrive() { @@ -91,7 +97,9 @@ final class NavigationController: NSObject, NavigationInterface { func requestRoutes(points: [Point]) async throws { - waypoints.append(Waypoint(coordinate: self.currentLocation!.coordinate, name: "Current location")) + if(self.currentLocation != nil) { + waypoints.append(Waypoint(coordinate: self.currentLocation!.coordinate, name: "Current location")) + } let provider = core.routingProvider() if shouldRequestMapMatching { @@ -164,7 +172,30 @@ final class NavigationController: NSObject, NavigationInterface { completion(.success(Void())) } - func lastLocation(completion: @escaping (Result) -> Void) { + func lastLocation(completion: @escaping (Result) -> Void) { + if(self.currentLocation != nil) + { + completion(.success(self.currentLocation!.toFLTNavigationLocation())) + } + else if (self.mapView.location.latestLocation != nil) { + let timestamp = Int64(self.mapView.location.latestLocation!.timestamp.timeIntervalSince1970) + + completion(.success(NavigationLocation( + latitude: self.mapView.location.latestLocation!.coordinate.latitude, + longitude: self.mapView.location.latestLocation!.coordinate.longitude, + timestamp: timestamp, + monotonicTimestamp: timestamp, + altitude: self.mapView.location.latestLocation!.altitude, + horizontalAccuracy: self.mapView.location.latestLocation!.horizontalAccuracy, + verticalAccuracy: self.mapView.location.latestLocation!.verticalAccuracy, + speed: self.mapView.location.latestLocation!.speed, + speedAccuracy: self.mapView.location.latestLocation!.speedAccuracy, + bearing: nil, + bearingAccuracy: nil, + floor: nil, + source: nil + ))) + } completion(.success(nil)) } } From 0f62d9beee0895b3236fc13c2b7c37a60e7a81a4 Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sat, 8 Feb 2025 08:40:46 +0100 Subject: [PATCH 18/33] right way to do this --- .../integration_test/empty_map_widget.dart | 2 +- example/lib/main.dart | 2 +- example/lib/navigator_example.dart | 20 +- ios/Classes/EnumsMapping.swift | 23 + ...ionController+ContinuousAlternatives.swift | 64 +++ .../NavigationController+Gestures.swift | 206 +++++++ ...igationController+VanishingRouteLine.swift | 212 +++++++ ios/Classes/NavigationController.swift | 381 ++++++++++++- ios/Classes/NavigationMapStyleManager.swift | 538 ++++++++++++++++++ 9 files changed, 1421 insertions(+), 27 deletions(-) create mode 100644 ios/Classes/NavigationController+ContinuousAlternatives.swift create mode 100644 ios/Classes/NavigationController+Gestures.swift create mode 100644 ios/Classes/NavigationController+VanishingRouteLine.swift create mode 100644 ios/Classes/NavigationMapStyleManager.swift diff --git a/example/integration_test/empty_map_widget.dart b/example/integration_test/empty_map_widget.dart index e5ae7dcf2..ab8ed174c 100644 --- a/example/integration_test/empty_map_widget.dart +++ b/example/integration_test/empty_map_widget.dart @@ -22,7 +22,7 @@ class Events { } var events = Events(); -const ACCESS_TOKEN = String.fromEnvironment('ACCESS_TOKEN'); +const ACCESS_TOKEN = "pk.eyJ1IjoicmlkZWhpa2UiLCJhIjoiY2xwc2wwNGZrMDN3eTJqcGwxdjViaGRzdiJ9.jFtcT-N-qh-Zj7i0vrWxAA"; Future main({double? width, double? height, CameraOptions? camera}) { final completer = Completer(); diff --git a/example/lib/main.dart b/example/lib/main.dart index d3762b381..3230f2703 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -64,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 = "pk.eyJ1IjoicmlkZWhpa2UiLCJhIjoiY2xwc2wwNGZrMDN3eTJqcGwxdjViaGRzdiJ9.jFtcT-N-qh-Zj7i0vrWxAA"; 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 index 4b405600c..c326dea29 100644 --- a/example/lib/navigator_example.dart +++ b/example/lib/navigator_example.dart @@ -43,8 +43,10 @@ class NavigatorExampleState extends State _onStyleLoadedCallback(StyleLoadedEventData data) async { print("Style loaded"); styleLoaded = true; - await mapboxMap.navigation.startTripSession(true); + //await mapboxMap.navigation.startTripSession(true); + print("Trip navigation started"); await _start(); + print("Trip started"); } _onNavigationRouteReadyListener() async { @@ -103,6 +105,7 @@ class NavigatorExampleState extends State 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(); @@ -116,8 +119,8 @@ class NavigatorExampleState extends State shadowImage: Uint8List.fromList([]))))); print("Puck enabled"); - var myCoordinate = await mapboxMap.style.getPuckPosition(); - if (myCoordinate == null) { + //var myCoordinate = await mapboxMap.style.getPuckPosition(); + //if (myCoordinate == null) { print("Puck location was not defined"); var lastLocation = await mapboxMap.navigation.lastLocation(); if (lastLocation == null) { @@ -125,11 +128,12 @@ class NavigatorExampleState extends State return; } - myCoordinate = Position(lastLocation.longitude!, lastLocation.latitude!); - } + 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); @@ -137,6 +141,8 @@ class NavigatorExampleState extends State Point(coordinates: myCoordinate), Point(coordinates: destinationCoordinate) ]); + + await mapboxMap.navigation.startTripSession(true); } @override @@ -161,13 +167,14 @@ class NavigatorExampleState extends State Center( child: SizedBox( width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height - 80, + 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), )), @@ -175,6 +182,7 @@ class NavigatorExampleState extends State padding: const EdgeInsets.fromLTRB(72, 8, 8, 8), child: FloatingActionButton( elevation: 5, + heroTag: "overview", onPressed: _onOverviewClicked, child: const Icon(Icons.route), )), 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/NavigationController+ContinuousAlternatives.swift b/ios/Classes/NavigationController+ContinuousAlternatives.swift new file mode 100644 index 000000000..bd39f3fa9 --- /dev/null +++ b/ios/Classes/NavigationController+ContinuousAlternatives.swift @@ -0,0 +1,64 @@ +import _MapboxNavigationHelpers +import Foundation +import MapboxDirections +import Turf +import UIKit + +extension NavigationController { + /// Returns a list of the ``AlternativeRoute``s, that are close to a certain point and are within threshold distance + /// defined in ``NavigationMapView/tapGestureDistanceThreshold``. + /// + /// - parameter point: Point on the screen. + /// - returns: List of the alternative routes, which were found. If there are no continuous alternatives routes on + /// the map view `nil` will be returned. + /// An empty array is returned if no alternative route was tapped or if there are multiple equally fitting + /// routes at the tap coordinate. + func continuousAlternativeRoutes(closeTo point: CGPoint) -> [AlternativeRoute]? { + guard let routes, !routes.alternativeRoutes.isEmpty + else { + return nil + } + + // Workaround for XCode 12.5 compilation bug + typealias RouteWithMetadata = (route: Route, index: Int, distance: LocationDistance) + + let continuousAlternatives = routes.alternativeRoutes + // Add the main route to detect if the main route is the closest to the point. The main route is excluded from + // the result array. + let allRoutes = [routes.mainRoute.route] + continuousAlternatives.map { $0.route } + + // Filter routes with at least 2 coordinates and within tap distance. + let tapCoordinate = mapView.mapboxMap.coordinate(for: point) + let routeMetadata = allRoutes.enumerated() + .compactMap { index, route -> RouteWithMetadata? in + guard route.shape?.coordinates.count ?? 0 > 1 else { + return nil + } + guard let closestCoordinate = route.shape?.closestCoordinate(to: tapCoordinate)?.coordinate else { + return nil + } + + let closestPoint = mapView.mapboxMap.point(for: closestCoordinate) + guard closestPoint.distance(to: point) < tapGestureDistanceThreshold else { + return nil + } + let distance = closestCoordinate.distance(to: tapCoordinate) + return RouteWithMetadata(route: route, index: index, distance: distance) + } + + // Sort routes by closest distance to tap gesture. + let closest = routeMetadata.sorted { (lhs: RouteWithMetadata, rhs: RouteWithMetadata) -> Bool in + return lhs.distance < rhs.distance + } + + // Exclude the routes if the distance is the same and we cannot distinguish the routes. + if routeMetadata.count > 1, abs(routeMetadata[0].distance - routeMetadata[1].distance) < 1e-6 { + return [] + } + + return closest.compactMap { (item: RouteWithMetadata) -> AlternativeRoute? in + guard item.index > 0 else { return nil } + return continuousAlternatives[item.index - 1] + } + } +} diff --git a/ios/Classes/NavigationController+Gestures.swift b/ios/Classes/NavigationController+Gestures.swift new file mode 100644 index 000000000..8897d4d49 --- /dev/null +++ b/ios/Classes/NavigationController+Gestures.swift @@ -0,0 +1,206 @@ +import _MapboxNavigationHelpers +import Foundation +import MapboxDirections +import MapboxMaps +import Turf +import UIKit + +extension NavigationController { + func setupGestureRecognizers() { + // Gesture recognizer, which is used to detect long taps on any point on the map. + let longPressGestureRecognizer = UILongPressGestureRecognizer( + target: self, + action: #selector(handleLongPress(_:)) + ) + addGestureRecognizer(longPressGestureRecognizer) + + // Gesture recognizer, which is used to detect taps on route line, waypoint or POI + mapViewTapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(didReceiveTap(gesture:)) + ) + mapViewTapGestureRecognizer.delegate = self + mapView.addGestureRecognizer(mapViewTapGestureRecognizer) + + makeGestureRecognizersDisableCameraFollowing() + makeTapGestureRecognizerStopAnimatedTransitions() + } + + @objc + private func handleLongPress(_ gesture: UIGestureRecognizer) { + guard gesture.state == .began else { return } + let gestureLocation = gesture.location(in: self) + Task { @MainActor in + let point = await mapPoint(at: gestureLocation) + delegate?.navigationMapView(self, userDidLongTap: point) + } + } + + /// Modifies `MapView` gesture recognizers to disable follow mode and move `NavigationCamera` to + /// `NavigationCameraState.idle` state. + private func makeGestureRecognizersDisableCameraFollowing() { + for gestureRecognizer in mapView.gestureRecognizers ?? [] + where gestureRecognizer is UIPanGestureRecognizer + || gestureRecognizer is UIRotationGestureRecognizer + || gestureRecognizer is UIPinchGestureRecognizer + || gestureRecognizer == mapView.gestures.doubleTapToZoomInGestureRecognizer + || gestureRecognizer == mapView.gestures.doubleTouchToZoomOutGestureRecognizer + + { + gestureRecognizer.addTarget(self, action: #selector(switchToIdleCamera)) + } + } + + private func makeTapGestureRecognizerStopAnimatedTransitions() { + for gestureRecognizer in mapView.gestureRecognizers ?? [] + where gestureRecognizer is UITapGestureRecognizer + && gestureRecognizer != mapView.gestures.doubleTouchToZoomOutGestureRecognizer + { + gestureRecognizer.addTarget(self, action: #selector(switchToIdleCameraIfNotFollowing)) + } + } + + @objc + private func switchToIdleCamera() { + update(navigationCameraState: .idle) + } + + @objc + private func switchToIdleCameraIfNotFollowing() { + guard navigationCamera.currentCameraState != .following else { return } + update(navigationCameraState: .idle) + } + + /// Fired when NavigationMapView detects a tap not handled elsewhere by other gesture recognizers. + @objc + private func didReceiveTap(gesture: UITapGestureRecognizer) { + guard gesture.state == .recognized else { return } + let tapPoint = gesture.location(in: mapView) + + Task { + if let allRoutes = routes?.allRoutes() { + let waypointTest = legSeparatingWaypoints(on: allRoutes, closeTo: tapPoint) + if let selected = waypointTest?.first { + delegate?.navigationMapView(self, didSelect: selected) + return + } + } + + if let alternativeRoute = continuousAlternativeRoutes(closeTo: tapPoint)?.first { + delegate?.navigationMapView(self, didSelect: alternativeRoute) + return + } + + let point = await mapPoint(at: tapPoint) + + if point.name != nil { + delegate?.navigationMapView(self, userDidTap: point) + } + } + } + + func legSeparatingWaypoints(on routes: [Route], closeTo point: CGPoint) -> [Waypoint]? { + // In case if route does not contain more than one leg - do nothing. + let multipointRoutes = routes.filter { $0.legs.count > 1 } + guard multipointRoutes.count > 0 else { return nil } + + let waypoints = multipointRoutes.compactMap { route in + route.legs.dropLast().compactMap { $0.destination } + }.flatMap { $0 } + + // Sort the array in order of closest to tap. + let tapCoordinate = mapView.mapboxMap.coordinate(for: point) + let closest = waypoints.sorted { left, right -> Bool in + let leftDistance = left.coordinate.projectedDistance(to: tapCoordinate) + let rightDistance = right.coordinate.projectedDistance(to: tapCoordinate) + return leftDistance < rightDistance + } + + // Filter to see which ones are under threshold. + let candidates = closest.filter { + let coordinatePoint = mapView.mapboxMap.point(for: $0.coordinate) + + return coordinatePoint.distance(to: point) < tapGestureDistanceThreshold + } + + return candidates + } + + private func mapPoint(at point: CGPoint) async -> MapPoint { + let options = RenderedQueryOptions(layerIds: mapStyleManager.poiLayerIds, filter: nil) + let rectSize = poiClickableAreaSize + let rect = CGRect(x: point.x - rectSize / 2, y: point.y - rectSize / 2, width: rectSize, height: rectSize) + + let features = try? await mapView.mapboxMap.queryRenderedFeatures(with: rect, options: options) + if let feature = features?.first?.queriedFeature.feature, + case .string(let poiName) = feature[property: .poiName, languageCode: nil], + case .point(let point) = feature.geometry + { + return MapPoint(name: poiName, coordinate: point.coordinates) + } else { + let coordinate = mapView.mapboxMap.coordinate(for: point) + return MapPoint(name: nil, coordinate: coordinate) + } + } +} + +// MARK: - GestureManagerDelegate + +extension NavigationController: GestureManagerDelegate { + public nonisolated func gestureManager( + _ gestureManager: MapboxMaps.GestureManager, + didBegin gestureType: MapboxMaps.GestureType + ) { + guard gestureType != .singleTap else { return } + + MainActor.assumingIsolated { + delegate?.navigationMapViewUserDidStartInteraction(self) + } + } + + public nonisolated func gestureManager( + _ gestureManager: MapboxMaps.GestureManager, + didEnd gestureType: MapboxMaps.GestureType, + willAnimate: Bool + ) { + guard gestureType != .singleTap else { return } + + MainActor.assumingIsolated { + delegate?.navigationMapViewUserDidEndInteraction(self) + } + } + + public nonisolated func gestureManager( + _ gestureManager: MapboxMaps.GestureManager, + didEndAnimatingFor gestureType: MapboxMaps.GestureType + ) {} +} + +// MARK: - UIGestureRecognizerDelegate + +extension NavigationController: UIGestureRecognizerDelegate { + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + if gestureRecognizer is UITapGestureRecognizer, + otherGestureRecognizer is UITapGestureRecognizer + { + return true + } + + return false + } + + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + if gestureRecognizer is UITapGestureRecognizer, + otherGestureRecognizer == mapView.gestures.doubleTapToZoomInGestureRecognizer + { + return true + } + return false + } +} diff --git a/ios/Classes/NavigationController+VanishingRouteLine.swift b/ios/Classes/NavigationController+VanishingRouteLine.swift new file mode 100644 index 000000000..747f7a247 --- /dev/null +++ b/ios/Classes/NavigationController+VanishingRouteLine.swift @@ -0,0 +1,212 @@ +import _MapboxNavigationHelpers +import CoreLocation +import MapboxDirections +import MapboxMaps +import UIKit + +extension NavigationController { + struct RoutePoints { + var nestedList: [[[CLLocationCoordinate2D]]] + var flatList: [CLLocationCoordinate2D] + } + + struct RouteLineGranularDistances { + var distance: Double + var distanceArray: [RouteLineDistancesIndex] + } + + struct RouteLineDistancesIndex { + var point: CLLocationCoordinate2D + var distanceRemaining: Double + } + + // MARK: Customizing and Displaying the Route Line(s) + + func initPrimaryRoutePoints(route: Route) { + routePoints = parseRoutePoints(route: route) + routeLineGranularDistances = calculateGranularDistances(routePoints?.flatList ?? []) + } + + /// Transform the route data into nested arrays of legs -> steps -> coordinates. + /// The first and last point of adjacent steps overlap and are duplicated. + func parseRoutePoints(route: Route) -> RoutePoints { + let nestedList = route.legs.map { (routeLeg: RouteLeg) -> [[CLLocationCoordinate2D]] in + return routeLeg.steps.map { (routeStep: RouteStep) -> [CLLocationCoordinate2D] in + if let routeShape = routeStep.shape { + return routeShape.coordinates + } else { + return [] + } + } + } + let flatList = nestedList.flatMap { $0.flatMap { $0.compactMap { $0 } } } + return RoutePoints(nestedList: nestedList, flatList: flatList) + } + + func updateRouteLine(routeProgress: RouteProgress) { + updateIntersectionAnnotations(routeProgress: routeProgress) + if let routes { + mapStyleManager.updateRouteAlertsAnnotations( + navigationRoutes: routes, + excludedRouteAlertTypes: excludedRouteAlertTypes, + distanceTraveled: routeProgress.distanceTraveled + ) + } + + if routeLineTracksTraversal, routes != nil { + guard !routeProgress.routeIsComplete else { + mapStyleManager.removeRoutes() + mapStyleManager.removeArrows() + return + } + + updateUpcomingRoutePointIndex(routeProgress: routeProgress) + } + updateArrow(routeProgress: routeProgress) + } + + func updateAlternatives(routeProgress: RouteProgress?) { + guard let routes = routeProgress?.navigationRoutes ?? routes else { return } + show(routes, routeAnnotationKinds: routeAnnotationKinds) + } + + func updateIntersectionAnnotations(routeProgress: RouteProgress?) { + if let routeProgress, showsIntersectionAnnotations { + mapStyleManager.updateIntersectionAnnotations(routeProgress: routeProgress) + } else { + mapStyleManager.removeIntersectionAnnotations() + } + } + + /// Find and cache the index of the upcoming [RouteLineDistancesIndex]. + func updateUpcomingRoutePointIndex(routeProgress: RouteProgress) { + guard let completeRoutePoints = routePoints, + completeRoutePoints.nestedList.indices.contains(routeProgress.legIndex) + else { + routeRemainingDistancesIndex = nil + return + } + let currentLegProgress = routeProgress.currentLegProgress + let currentStepProgress = routeProgress.currentLegProgress.currentStepProgress + let currentLegSteps = completeRoutePoints.nestedList[routeProgress.legIndex] + var allRemainingPoints = 0 + // Find the count of remaining points in the current step. + let lineString = currentStepProgress.step.shape ?? LineString([]) + // If user hasn't arrived at current step. All the coordinates will be included to the remaining points. + if currentStepProgress.distanceTraveled < 0 { + allRemainingPoints += currentLegSteps[currentLegProgress.stepIndex].count + } else if let startIndex = lineString + .indexedCoordinateFromStart(distance: currentStepProgress.distanceTraveled)?.index, + lineString.coordinates.indices.contains(startIndex) + { + allRemainingPoints += lineString.coordinates.suffix(from: startIndex + 1).dropLast().count + } + + // Add to the count of remaining points all of the remaining points on the current leg, after the current step. + if currentLegProgress.stepIndex < currentLegSteps.endIndex { + var count = 0 + for stepIndex in (currentLegProgress.stepIndex + 1).. RouteLineGranularDistances? { + if coordinates.isEmpty { return nil } + var distance = 0.0 + var indexArray = [RouteLineDistancesIndex?](repeating: nil, count: coordinates.count) + for index in stride(from: coordinates.count - 1, to: 0, by: -1) { + let curr = coordinates[index] + let prev = coordinates[index - 1] + distance += curr.projectedDistance(to: prev) + indexArray[index - 1] = RouteLineDistancesIndex(point: prev, distanceRemaining: distance) + } + indexArray[coordinates.count - 1] = RouteLineDistancesIndex( + point: coordinates[coordinates.count - 1], + distanceRemaining: 0.0 + ) + return RouteLineGranularDistances(distance: distance, distanceArray: indexArray.compactMap { $0 }) + } + + func findClosestCoordinateOnCurrentLine( + coordinate: CLLocationCoordinate2D, + granularDistances: RouteLineGranularDistances, + upcomingIndex: Int + ) -> CLLocationCoordinate2D { + guard granularDistances.distanceArray.indices.contains(upcomingIndex) else { return coordinate } + + var coordinates = [CLLocationCoordinate2D]() + + // Takes the passed 10 points and the upcoming point of route to form a sliced polyline for distance + // calculation, incase of the curved shape of route. + for index in max(0, upcomingIndex - 10)...upcomingIndex { + let point = granularDistances.distanceArray[index].point + coordinates.append(point) + } + + let polyline = LineString(coordinates) + + return polyline.closestCoordinate(to: coordinate)?.coordinate ?? coordinate + } + + /// Updates the fractionTraveled along the route line from the origin point to the indicated point. + /// + /// - parameter coordinate: Current position of the user location. + func calculateFractionTraveled(coordinate: CLLocationCoordinate2D) -> Double? { + guard let granularDistances = routeLineGranularDistances, + let index = routeRemainingDistancesIndex, + granularDistances.distanceArray.indices.contains(index) else { return nil } + let traveledIndex = granularDistances.distanceArray[index] + let upcomingPoint = traveledIndex.point + + // Project coordinate onto current line to properly find offset without an issue of back-growing route line. + let coordinate = findClosestCoordinateOnCurrentLine( + coordinate: coordinate, + granularDistances: granularDistances, + upcomingIndex: index + 1 + ) + + // Take the remaining distance from the upcoming point on the route and extends it by the exact position of the + // puck. + let remainingDistance = traveledIndex.distanceRemaining + upcomingPoint.projectedDistance(to: coordinate) + + // Calculate the percentage of the route traveled. + if granularDistances.distance > 0 { + let offset = (1.0 - remainingDistance / granularDistances.distance) + if offset >= 0 { + return offset + } else { + return nil + } + } + return nil + } + + /// Updates the route style layer and its casing style layer to gradually disappear as the user location puck + /// travels along the displayed route. + /// + /// - parameter coordinate: Current position of the user location. + func travelAlongRouteLine(to coordinate: CLLocationCoordinate2D?) { + guard let coordinate, routes != nil else { return } + if let fraction = calculateFractionTraveled(coordinate: coordinate) { + mapStyleManager.setRouteLineOffset(fraction, for: .main) + } + } +} diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index 69d47fca2..a7c73ef9f 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -3,17 +3,24 @@ import CoreLocation import MapboxDirections import MapboxNavigationCore import MapboxMaps +import _MapboxNavigationHelpers @MainActor final class NavigationController: NSObject, NavigationInterface { + + private enum Constants { + static let initialMapRect = CGRect(x: 0, y: 0, width: 64, height: 64) + static let initialViewportPadding = UIEdgeInsets(top: 20, left: 20, bottom: 40, right: 20) + } + let predictiveCacheManager: PredictiveCacheManager? @Published private(set) var isInActiveNavigation: Bool = false @Published private(set) var currentPreviewRoutes: MapboxNavigationCore.NavigationRoutes? @Published private(set) var activeNavigationRoutes: MapboxNavigationCore.NavigationRoutes? - @Published private(set) var routeProgress: MapboxNavigationCore.RouteProgress? + @Published private(set) var currentRouteProgress: MapboxNavigationCore.RouteProgress? @Published private(set) var currentLocation: CLLocation? - @Published var cameraState: NavigationCameraState = .iDLE + @Published var cameraState: MapboxNavigationCore.NavigationCameraState = .idle @Published var profileIdentifier: ProfileIdentifier = .automobileAvoidingTraffic @Published var shouldRequestMapMatching = false @@ -24,6 +31,10 @@ final class NavigationController: NSObject, NavigationInterface { private var onNavigationListener: NavigationListener? private let mapView: MapView private let navigationProvider: MapboxNavigationProvider + private var navigationCamera: NavigationCamera + private let mapStyleManager: NavigationMapStyleManager + + private var lifetimeSubscriptions: Set = [] init(withMapView mapView: MapView, navigationProvider: MapboxNavigationProvider) { @@ -34,9 +45,16 @@ final class NavigationController: NSObject, NavigationInterface { self.core = self.navigationProvider.mapboxNavigation self.predictiveCacheManager = self.navigationProvider.predictiveCacheManager + self.mapStyleManager = .init(mapView: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) + self.navigationCamera = NavigationCamera( + mapView, + location: core.navigation().locationMatching.map(\.enhancedLocation).eraseToAnyPublisher(), + routeProgress: core.navigation().routeProgress.map(\.?.routeProgress).eraseToAnyPublisher()) + super.init() observeNavigation() + observeCamera() } private func observeNavigation() { @@ -71,7 +89,337 @@ final class NavigationController: NSObject, NavigationInterface { } } } + + private func observeCamera() { + navigationCamera.cameraStates + .sink { [weak self] cameraState in + guard let self else { return } + self.cameraState = cameraState + self.onNavigationListener?.onNavigationCameraStateChanged(state: self.cameraState.toFLTNavigationCameraState()!) {_ in } + } + } + + private var customRouteLineLayerPosition: MapboxMaps.LayerPosition? = nil { + didSet { + mapStyleManager.customRouteLineLayerPosition = customRouteLineLayerPosition + guard let activeNavigationRoutes else { return } + show(activeNavigationRoutes, routeAnnotationKinds: routeAnnotationKinds) + } + } + + private(set) var routeAnnotationKinds: Set = [] + + // MARK: - Public configuration + + /// The padding applied to the viewport in addition to the safe area. + public var viewportPadding: UIEdgeInsets = Constants.initialViewportPadding { + didSet { updateCameraPadding() } + } + + @_spi(MapboxInternal) public var showsViewportDebugView: Bool = false { + didSet { updateDebugViewportVisibility() } + } + + /// Controls whether to show annotations on intersections, e.g. traffic signals, railroad crossings, yield and stop + /// signs. Defaults to `true`. + public var showsIntersectionAnnotations: Bool = true { + didSet { + updateIntersectionAnnotations(routeProgress: currentRouteProgress) + } + } + + /// Toggles displaying alternative routes. If enabled, view will draw actual alternative route lines on the map. + /// Defaults to `true`. + public var showsAlternatives: Bool = true { + didSet { + updateAlternatives(routeProgress: currentRouteProgress) + } + } + + /// Toggles displaying relative ETA callouts on alternative routes, during active guidance. + /// Defaults to `true`. + public var showsRelativeDurationsOnAlternativeManuever: Bool = true { + didSet { + if showsRelativeDurationsOnAlternativeManuever { + routeAnnotationKinds = [.relativeDurationsOnAlternativeManuever] + } else { + routeAnnotationKinds.removeAll() + } + updateAlternatives(routeProgress: currentRouteProgress) + } + } + + /// Controls whether the main route style layer and its casing disappears as the user location puck travels over it. + /// Defaults to `true`. + /// + /// If `true`, the part of the route that has been traversed will be rendered with full transparency, to give the + /// illusion of a disappearing route. If `false`, the whole route will be shown without traversed part disappearing + /// effect. + public var routeLineTracksTraversal: Bool = true + + /// The maximum distance (in screen points) the user can tap for a selection to be valid when selecting a POI. + public var poiClickableAreaSize: CGFloat = 40 + + /// Controls whether to show restricted portions of a route line. Defaults to true. + public var showsRestrictedAreasOnRoute: Bool = true + + /// Decreases route line opacity based on occlusion from 3D objects. + /// Value `0` disables occlusion, value `1` means fully occluded. Defaults to `0.85`. + public var routeLineOcclusionFactor: Double = 0.85 + + /// Configuration for displaying congestion levels on the route line. + /// Allows to customize the congestion colors and ranges that represent different congestion levels. + public var congestionConfiguration: CongestionConfiguration = .default + + /// Controls whether the traffic should be drawn on the route line or not. Defaults to true. + public var showsTrafficOnRouteLine: Bool = true + + /// Maximum distance (in screen points) the user can tap for a selection to be valid when selecting an alternate + /// route. + public var tapGestureDistanceThreshold: CGFloat = 50 + + /// Controls whether intermediate waypoints displayed on the route line. Defaults to `true`. + public var showsIntermediateWaypoints: Bool = true { + didSet { + updateWaypointsVisiblity() + } + } + + public func update(navigationCameraState: MapboxNavigationCore.NavigationCameraState) { + guard cameraState != navigationCamera.currentCameraState else { return } + navigationCamera.update(cameraState: navigationCameraState) + } + + /// Visualizes the given routes and it's alternatives, removing any existing from the map. + /// + /// Each route is visualized as a line. Each line is color-coded by traffic congestion, if congestion + /// levels are present. To also visualize waypoints and zoom the map to fit, + /// use the ``showcase(_:routesPresentationStyle:routeAnnotationKinds:animated:duration:)`` method. + /// + /// To undo the effects of this method, use ``removeRoutes()`` method. + /// - Parameters: + /// - navigationRoutes: ``NavigationRoutes`` to be displayed on the map. + /// - routeAnnotationKinds: A set of ``RouteAnnotationKind`` that should be displayed. + public func show( + _ navigationRoutes: NavigationRoutes, + routeAnnotationKinds: Set + ) { + removeRoutes() + activeNavigationRoutes = navigationRoutes + self.routeAnnotationKinds = routeAnnotationKinds + let mainRoute = navigationRoutes.mainRoute.route + if routeLineTracksTraversal { + initPrimaryRoutePoints(route: mainRoute) + } + mapStyleManager.updateRoutes( + navigationRoutes, + config: mapStyleConfig, + featureProvider: customRouteLineFeatureProvider + ) + updateWaypointsVisiblity() + + mapStyleManager.updateRouteAnnotations( + navigationRoutes: navigationRoutes, + annotationKinds: routeAnnotationKinds, + config: mapStyleConfig + ) + mapStyleManager.updateRouteAlertsAnnotations( + navigationRoutes: navigationRoutes, + excludedRouteAlertTypes: excludedRouteAlertTypes + ) + } + + /// Removes routes and all visible annotations from the map. + public func removeRoutes() { + activeNavigationRoutes = nil + routeLineGranularDistances = nil + routeRemainingDistancesIndex = nil + mapStyleManager.removeAllFeatures() + } + + func updateArrow(routeProgress: RouteProgress) { + if routeProgress.currentLegProgress.followOnStep != nil { + mapStyleManager.updateArrows( + route: routeProgress.route, + legIndex: routeProgress.legIndex, + stepIndex: routeProgress.currentLegProgress.stepIndex + 1, + config: mapStyleConfig + ) + } else { + removeArrows() + } + } + + /// Removes the `RouteStep` arrow from the `MapView`. + func removeArrows() { + mapStyleManager.removeArrows() + } + + // MARK: - Debug Viewport + + private func updateDebugViewportVisibility() { + if showsViewportDebugView { + let viewportDebugView = with(UIView(frame: .zero)) { + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.blue.cgColor + $0.backgroundColor = .clear + } + addSubview(viewportDebugView) + self.viewportDebugView = viewportDebugView + viewportDebugView.isUserInteractionEnabled = false + updateViewportDebugView() + } else { + viewportDebugView?.removeFromSuperview() + viewportDebugView = nil + } + } + + private func updateViewportDebugView() { + viewportDebugView?.frame = bounds.inset(by: navigationCamera.viewportPadding) + } + + // MARK: - Camera + + private func updateCameraPadding() { + let padding = viewportPadding + let safeAreaInsets = safeAreaInsets + + navigationCamera.viewportPadding = .init( + top: safeAreaInsets.top + padding.top, + left: safeAreaInsets.left + padding.left, + bottom: safeAreaInsets.bottom + padding.bottom, + right: safeAreaInsets.right + padding.right + ) + updateViewportDebugView() + } + + private func fitCamera( + routes: NavigationRoutes, + routesPresentationStyle: RoutesPresentationStyle, + animated: Bool = false, + duration: TimeInterval + ) { + navigationCamera.stop() + let coordinates: [CLLocationCoordinate2D] + switch routesPresentationStyle { + case .main, .all(shouldFit: false): + coordinates = routes.mainRoute.route.shape?.coordinates ?? [] + case .all(true): + let routes = [routes.mainRoute.route] + routes.alternativeRoutes.map(\.route) + coordinates = MultiLineString(routes.compactMap(\.shape?.coordinates)).coordinates.flatMap { $0 } + } + let initialCameraOptions = CameraOptions( + padding: navigationCamera.viewportPadding, + bearing: 0, + pitch: 0 + ) + do { + let cameraOptions = try mapView.mapboxMap.camera( + for: coordinates, + camera: initialCameraOptions, + coordinatesPadding: nil, + maxZoom: nil, + offset: nil + ) + mapView.camera.ease(to: cameraOptions, duration: animated ? duration : 0.0) + } catch { + Log.error("Failed to fit the camera: \(error.localizedDescription)", category: .navigationUI) + } + } + + + private var waypointsFeatureProvider: WaypointFeatureProvider { + .init { [weak self] waypoints, legIndex in + guard let self else { return nil } + return delegate?.navigationMapView(self, shapeFor: waypoints, legIndex: legIndex) + } customCirleLayer: { [weak self] identifier, sourceIdentifier in + guard let self else { return nil } + return delegate?.navigationMapView( + self, + waypointCircleLayerWithIdentifier: identifier, + sourceIdentifier: sourceIdentifier + ) + } customSymbolLayer: { [weak self] identifier, sourceIdentifier in + guard let self else { return nil } + return delegate?.navigationMapView( + self, + waypointSymbolLayerWithIdentifier: identifier, + sourceIdentifier: sourceIdentifier + ) + } + } + + private func updateWaypointsVisiblity() { + guard let mainRoute = routes?.mainRoute.route else { + mapStyleManager.removeWaypoints() + return + } + mapStyleManager.updateWaypoints( + route: mainRoute, + legIndex: currentRouteProgress?.legIndex ?? 0, + config: mapStyleConfig, + featureProvider: waypointsFeatureProvider + ) + } + + // MARK: Configuring Cache and Tiles Storage + + private var predictiveCacheMapObserver: MapboxMaps.Cancelable? = nil + + /// Setups the Predictive Caching mechanism using provided Options. + /// + /// This will handle all the required manipulations to enable the feature and maintain it during the navigations. + /// Once enabled, it will be present as long as `NavigationMapView` is retained. + /// + /// - parameter options: options, controlling caching parameters like area radius and concurrent downloading + /// threads. + private func enablePredictiveCaching(with predictiveCacheManager: PredictiveCacheManager?) { + predictiveCacheMapObserver?.cancel() + + guard let predictiveCacheManager else { + predictiveCacheMapObserver = nil + return + } + + predictiveCacheManager.updateMapControllers(mapView: mapView) + predictiveCacheMapObserver = mapView.mapboxMap.onStyleLoaded.observe { [ + weak self, + predictiveCacheManager + ] _ in + guard let self else { return } + + predictiveCacheManager.updateMapControllers(mapView: mapView) + } + } + + private var mapStyleConfig: MapStyleConfig { + .init( + routeCasingColor: routeCasingColor, + routeAlternateCasingColor: routeAlternateCasingColor, + routeRestrictedAreaColor: routeRestrictedAreaColor, + traversedRouteColor: traversedRouteColor, + maneuverArrowColor: maneuverArrowColor, + maneuverArrowStrokeColor: maneuverArrowStrokeColor, + routeAnnotationSelectedColor: routeAnnotationSelectedColor, + routeAnnotationColor: routeAnnotationColor, + routeAnnotationSelectedTextColor: routeAnnotationSelectedTextColor, + routeAnnotationTextColor: routeAnnotationTextColor, + routeAnnotationMoreTimeTextColor: routeAnnotationMoreTimeTextColor, + routeAnnotationLessTimeTextColor: routeAnnotationLessTimeTextColor, + routeAnnotationTextFont: routeAnnotationTextFont, + routeLineTracksTraversal: routeLineTracksTraversal, + isRestrictedAreaEnabled: showsRestrictedAreasOnRoute, + showsTrafficOnRouteLine: showsTrafficOnRouteLine, + showsAlternatives: showsAlternatives, + showsIntermediateWaypoints: showsIntermediateWaypoints, + occlusionFactor: .constant(routeLineOcclusionFactor), + congestionConfiguration: congestionConfiguration, + waypointColor: waypointColor, + waypointStrokeColor: waypointStrokeColor + ) + } + func startFreeDrive() { core.tripSession().startFreeDrive() } @@ -79,28 +427,27 @@ final class NavigationController: NSObject, NavigationInterface { func cancelPreview() { waypoints = [] currentPreviewRoutes = nil - cameraState = .fOLLOWING + update(navigationCameraState: .following) } func startActiveNavigation() { guard let previewRoutes = currentPreviewRoutes else { return } core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) - cameraState = .fOLLOWING currentPreviewRoutes = nil waypoints = [] + update(navigationCameraState: .following) } func stopActiveNavigation() { core.tripSession().startFreeDrive() - cameraState = .fOLLOWING + update(navigationCameraState: .following) } func requestRoutes(points: [Point]) async throws { - - if(self.currentLocation != nil) { - waypoints.append(Waypoint(coordinate: self.currentLocation!.coordinate, name: "Current location")) - } - + guard !isInActiveNavigation, let currentLocation else { return } + + self.waypoints = points.map { Waypoint(coordinate: LocationCoordinate2D(latitude: $0.coordinates.latitude, longitude: $0.coordinates.longitude)) } + let provider = core.routingProvider() if shouldRequestMapMatching { let mapMatchingOptions = NavigationMatchOptions( @@ -119,7 +466,7 @@ final class NavigationController: NSObject, NavigationInterface { currentPreviewRoutes = previewRoutes self.onNavigationListener?.onNavigationRouteReady() { _ in } } - cameraState = .iDLE + update(navigationCameraState: .idle) } func addListeners(messenger: SuffixBinaryMessenger) { @@ -145,30 +492,26 @@ final class NavigationController: NSObject, NavigationInterface { func stopTripSession(completion: @escaping (Result) -> Void) { - cameraState = .oVERVIEW - self.onNavigationListener?.onNavigationCameraStateChanged(state: cameraState) {_ in } + update(navigationCameraState: .overview) completion(.success(Void())) } func startTripSession(withForegroundService: Bool, completion: @escaping (Result) -> Void) { guard let previewRoutes = currentPreviewRoutes else { return } core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) - cameraState = .fOLLOWING + update(navigationCameraState: .following) currentPreviewRoutes = nil waypoints = [] - self.onNavigationListener?.onNavigationCameraStateChanged(state: cameraState) {_ in } completion(.success(Void())) } func requestNavigationCameraToFollowing(completion: @escaping (Result) -> Void) { - cameraState = .fOLLOWING - self.onNavigationListener?.onNavigationCameraStateChanged(state: cameraState) {_ in } + update(navigationCameraState: .following) completion(.success(Void())) } func requestNavigationCameraToOverview(completion: @escaping (Result) -> Void) { - cameraState = .oVERVIEW - self.onNavigationListener?.onNavigationCameraStateChanged(state: cameraState) {_ in } + update(navigationCameraState: .overview) completion(.success(Void())) } diff --git a/ios/Classes/NavigationMapStyleManager.swift b/ios/Classes/NavigationMapStyleManager.swift new file mode 100644 index 000000000..2dd708d2c --- /dev/null +++ b/ios/Classes/NavigationMapStyleManager.swift @@ -0,0 +1,538 @@ +import Combine +import MapboxDirections +@_spi(Experimental) import MapboxMaps +import enum SwiftUI.ColorScheme +import UIKit +import MapboxNavigationCore + +struct CustomizedLayerProvider { + var customizedLayer: (Layer) -> Layer +} + +struct MapStyleConfig: Equatable { + var routeCasingColor: UIColor + var routeAlternateCasingColor: UIColor + var routeRestrictedAreaColor: UIColor + var traversedRouteColor: UIColor? + var maneuverArrowColor: UIColor + var maneuverArrowStrokeColor: UIColor + + var routeAnnotationSelectedColor: UIColor + var routeAnnotationColor: UIColor + var routeAnnotationSelectedTextColor: UIColor + var routeAnnotationTextColor: UIColor + var routeAnnotationMoreTimeTextColor: UIColor + var routeAnnotationLessTimeTextColor: UIColor + var routeAnnotationTextFont: UIFont + + var routeLineTracksTraversal: Bool + var isRestrictedAreaEnabled: Bool + var showsTrafficOnRouteLine: Bool + var showsAlternatives: Bool + var showsIntermediateWaypoints: Bool + var occlusionFactor: Value? + var congestionConfiguration: CongestionConfiguration + + var waypointColor: UIColor + var waypointStrokeColor: UIColor +} + +/// Manages all the sources/layers used in NavigationMap. +@MainActor +final class NavigationMapStyleManager { + private let mapView: MapView + private var lifetimeSubscriptions: Set = [] + private var layersOrder: MapLayersOrder + private var layerIds: [String] + + var customizedLayerProvider: CustomizedLayerProvider = .init { $0 } + var customRouteLineLayerPosition: LayerPosition? + + private let routeFeaturesStore: MapFeaturesStore + private let waypointFeaturesStore: MapFeaturesStore + private let arrowFeaturesStore: MapFeaturesStore + private let voiceInstructionFeaturesStore: MapFeaturesStore + private let intersectionAnnotationsFeaturesStore: MapFeaturesStore + private let routeAnnotationsFeaturesStore: MapFeaturesStore + private let routeAlertsFeaturesStore: MapFeaturesStore + + init(mapView: MapView, customRouteLineLayerPosition: LayerPosition?) { + self.mapView = mapView + self.layersOrder = Self.makeMapLayersOrder( + with: mapView, + customRouteLineLayerPosition: customRouteLineLayerPosition + ) + self.layerIds = mapView.mapboxMap.allLayerIdentifiers.map(\.id) + self.routeFeaturesStore = .init(mapView: mapView) + self.waypointFeaturesStore = .init(mapView: mapView) + self.arrowFeaturesStore = .init(mapView: mapView) + self.voiceInstructionFeaturesStore = .init(mapView: mapView) + self.intersectionAnnotationsFeaturesStore = .init(mapView: mapView) + self.routeAnnotationsFeaturesStore = .init(mapView: mapView) + self.routeAlertsFeaturesStore = .init(mapView: mapView) + + mapView.mapboxMap.onStyleLoaded.sink { [weak self] _ in + self?.onStyleLoaded() + }.store(in: &lifetimeSubscriptions) + } + + func onStyleLoaded() { + // MapsSDK removes all layers when a style is loaded, so we have to recreate MapLayersOrder. + layersOrder = Self.makeMapLayersOrder(with: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) + layerIds = mapView.mapboxMap.allLayerIdentifiers.map(\.id) + layersOrder.setStyleIds(layerIds) + + routeFeaturesStore.styleLoaded(order: &layersOrder) + waypointFeaturesStore.styleLoaded(order: &layersOrder) + arrowFeaturesStore.styleLoaded(order: &layersOrder) + voiceInstructionFeaturesStore.styleLoaded(order: &layersOrder) + intersectionAnnotationsFeaturesStore.styleLoaded(order: &layersOrder) + routeAnnotationsFeaturesStore.styleLoaded(order: &layersOrder) + routeAlertsFeaturesStore.styleLoaded(order: &layersOrder) + } + + func updateRoutes( + _ routes: NavigationRoutes, + config: MapStyleConfig, + featureProvider: RouteLineFeatureProvider + ) { + routeFeaturesStore.update( + using: routeLineMapFeatures( + routes: routes, + config: config, + featureProvider: featureProvider, + customizedLayerProvider: customizedLayerProvider + ), + order: &layersOrder + ) + } + + func updateWaypoints( + route: Route, + legIndex: Int, + config: MapStyleConfig, + featureProvider: WaypointFeatureProvider + ) { + let waypoints = route.waypointsMapFeature( + mapView: mapView, + legIndex: legIndex, + config: config, + featureProvider: featureProvider, + customizedLayerProvider: customizedLayerProvider + ) + waypointFeaturesStore.update( + using: waypoints.map { [$0] } ?? [], + order: &layersOrder + ) + } + + func updateArrows( + route: Route, + legIndex: Int, + stepIndex: Int, + config: MapStyleConfig + ) { + guard route.containsStep(at: legIndex, stepIndex: stepIndex) + else { + removeArrows(); return + } + + arrowFeaturesStore.update( + using: route.maneuverArrowMapFeatures( + ids: .nextArrow(), + cameraZoom: mapView.mapboxMap.cameraState.zoom, + legIndex: legIndex, + stepIndex: stepIndex, + config: config, + customizedLayerProvider: customizedLayerProvider + ), + order: &layersOrder + ) + } + + func updateVoiceInstructions(route: Route) { + voiceInstructionFeaturesStore.update( + using: route.voiceInstructionMapFeatures( + ids: .init(), + customizedLayerProvider: customizedLayerProvider + ), + order: &layersOrder + ) + } + + func updateIntersectionAnnotations(routeProgress: RouteProgress) { + intersectionAnnotationsFeaturesStore.update( + using: routeProgress.intersectionAnnotationsMapFeatures( + ids: .currentRoute, + customizedLayerProvider: customizedLayerProvider + ), + order: &layersOrder + ) + } + + func updateRouteAnnotations( + navigationRoutes: NavigationRoutes, + annotationKinds: Set, + config: MapStyleConfig + ) { + routeAnnotationsFeaturesStore.update( + using: navigationRoutes.routeDurationMapFeatures( + annotationKinds: annotationKinds, + config: config + ), + order: &layersOrder + ) + } + + func updateRouteAlertsAnnotations( + navigationRoutes: NavigationRoutes, + excludedRouteAlertTypes: RoadAlertType, + distanceTraveled: CLLocationDistance = 0.0 + ) { + routeAlertsFeaturesStore.update( + using: navigationRoutes.routeAlertsAnnotationsMapFeatures( + ids: .default, + distanceTraveled: distanceTraveled, + customizedLayerProvider: customizedLayerProvider, + excludedRouteAlertTypes: excludedRouteAlertTypes + ), + order: &layersOrder + ) + } + + func updateFreeDriveAlertsAnnotations( + roadObjects: [RoadObjectAhead], + excludedRouteAlertTypes: RoadAlertType, + distanceTraveled: CLLocationDistance = 0.0 + ) { + guard !roadObjects.isEmpty else { + return removeRoadAlertsAnnotations() + } + routeAlertsFeaturesStore.update( + using: roadObjects.routeAlertsAnnotationsMapFeatures( + ids: .default, + distanceTraveled: distanceTraveled, + customizedLayerProvider: customizedLayerProvider, + excludedRouteAlertTypes: excludedRouteAlertTypes + ), + order: &layersOrder + ) + } + + func removeRoutes() { + routeFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeWaypoints() { + waypointFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeArrows() { + arrowFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeVoiceInstructions() { + voiceInstructionFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeIntersectionAnnotations() { + intersectionAnnotationsFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeRouteAnnotations() { + routeAnnotationsFeaturesStore.update(using: nil, order: &layersOrder) + } + + private func removeRoadAlertsAnnotations() { + routeAlertsFeaturesStore.update(using: nil, order: &layersOrder) + } + + func removeAllFeatures() { + removeRoutes() + removeWaypoints() + removeArrows() + removeVoiceInstructions() + removeIntersectionAnnotations() + removeRouteAnnotations() + removeRoadAlertsAnnotations() + } + + private func routeLineMapFeatures( + routes: NavigationRoutes, + config: MapStyleConfig, + featureProvider: RouteLineFeatureProvider, + customizedLayerProvider: CustomizedLayerProvider + ) -> [any MapFeature] { + var features: [any MapFeature] = [] + + features.append(contentsOf: routes.mainRoute.route.routeLineMapFeatures( + ids: .main, + offset: 0, + isSoftGradient: true, + isAlternative: false, + config: config, + featureProvider: featureProvider, + customizedLayerProvider: customizedLayerProvider + )) + + if config.showsAlternatives { + for (idx, alternativeRoute) in routes.alternativeRoutes.enumerated() { + let deviationOffset = alternativeRoute.deviationOffset() + features.append(contentsOf: alternativeRoute.route.routeLineMapFeatures( + ids: .alternative(idx: idx), + offset: deviationOffset, + isSoftGradient: true, + isAlternative: true, + config: config, + featureProvider: featureProvider, + customizedLayerProvider: customizedLayerProvider + )) + } + } + + return features + } + + func setRouteLineOffset( + _ offset: Double, + for routeLineIds: FeatureIds.RouteLine + ) { + mapView.mapboxMap.setRouteLineOffset(offset, for: routeLineIds) + } + + private static func makeMapLayersOrder( + with mapView: MapView, + customRouteLineLayerPosition: LayerPosition? + ) -> MapLayersOrder { + let alternative_0_ids = FeatureIds.RouteLine.alternative(idx: 0) + let alternative_1_ids = FeatureIds.RouteLine.alternative(idx: 1) + let mainLineIds = FeatureIds.RouteLine.main + let arrowIds = FeatureIds.ManeuverArrow.nextArrow() + let waypointIds = FeatureIds.RouteWaypoints.default + let voiceInstructionIds = FeatureIds.VoiceInstruction.currentRoute + let intersectionIds = FeatureIds.IntersectionAnnotation.currentRoute + let routeAlertIds = FeatureIds.RouteAlertAnnotation.default + typealias R = MapLayersOrder.Rule + typealias SlottedRules = MapLayersOrder.SlottedRules + + let allSlotIdentifiers = mapView.mapboxMap.allSlotIdentifiers + let containsMiddleSlot = Slot.middle.map(allSlotIdentifiers.contains) ?? false + let legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? = containsMiddleSlot ? nil : { + legacyLayerPosition(for: $0, mapView: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) + } + + return MapLayersOrder( + builder: { + SlottedRules(.middle) { + R.orderedIds([ + alternative_0_ids.casing, + alternative_0_ids.main, + ]) + R.orderedIds([ + alternative_1_ids.casing, + alternative_1_ids.main, + ]) + R.orderedIds([ + mainLineIds.traversedRoute, + mainLineIds.casing, + mainLineIds.main, + ]) + R.orderedIds([ + arrowIds.arrowStroke, + arrowIds.arrow, + arrowIds.arrowSymbolCasing, + arrowIds.arrowSymbol, + ]) + R.orderedIds([ + alternative_0_ids.restrictedArea, + alternative_1_ids.restrictedArea, + mainLineIds.restrictedArea, + ]) + /// To show on top of arrows + R.hasPrefix("poi") + R.orderedIds([ + voiceInstructionIds.layer, + voiceInstructionIds.circleLayer, + ]) + } + // Setting the top position on the map. We cannot explicitly set `.top` position because `.top` + // renders behind Place and Transit labels + SlottedRules(nil) { + R.orderedIds([ + intersectionIds.layer, + routeAlertIds.layer, + waypointIds.innerCircle, + waypointIds.markerIcon, + NavigationMapView.LayerIdentifier.puck2DLayer, + NavigationMapView.LayerIdentifier.puck3DLayer, + ]) + } + }, + legacyPosition: legacyPosition + ) + } + + private static func legacyLayerPosition( + for layerIdentifier: String, + mapView: MapView, + customRouteLineLayerPosition: LayerPosition? + ) -> MapboxMaps.LayerPosition? { + let mainLineIds = FeatureIds.RouteLine.main + if layerIdentifier.hasPrefix(mainLineIds.main), + let customRouteLineLayerPosition, + !mapView.mapboxMap.allLayerIdentifiers.contains(where: { $0.id.hasPrefix(mainLineIds.main) }) + { + return customRouteLineLayerPosition + } + + let alternative_0_ids = FeatureIds.RouteLine.alternative(idx: 0) + let alternative_1_ids = FeatureIds.RouteLine.alternative(idx: 1) + let arrowIds = FeatureIds.ManeuverArrow.nextArrow() + let waypointIds = FeatureIds.RouteWaypoints.default + let voiceInstructionIds = FeatureIds.VoiceInstruction.currentRoute + let intersectionIds = FeatureIds.IntersectionAnnotation.currentRoute + let routeAlertIds = FeatureIds.RouteAlertAnnotation.default + + let lowermostSymbolLayers: [String] = [ + alternative_0_ids.casing, + alternative_0_ids.main, + alternative_1_ids.casing, + alternative_1_ids.main, + mainLineIds.traversedRoute, + mainLineIds.casing, + mainLineIds.main, + mainLineIds.restrictedArea, + ].compactMap { $0 } + let aboveRoadLayers: [String] = [ + arrowIds.arrowStroke, + arrowIds.arrow, + arrowIds.arrowSymbolCasing, + arrowIds.arrowSymbol, + intersectionIds.layer, + routeAlertIds.layer, + waypointIds.innerCircle, + waypointIds.markerIcon, + ] + let uppermostSymbolLayers: [String] = [ + voiceInstructionIds.layer, + voiceInstructionIds.circleLayer, + NavigationMapView.LayerIdentifier.puck2DLayer, + NavigationMapView.LayerIdentifier.puck3DLayer, + ] + let isLowermostLayer = lowermostSymbolLayers.contains(layerIdentifier) + let isAboveRoadLayer = aboveRoadLayers.contains(layerIdentifier) + let allAddedLayers: [String] = lowermostSymbolLayers + aboveRoadLayers + uppermostSymbolLayers + + var layerPosition: MapboxMaps.LayerPosition? + var lowerLayers = Set() + var upperLayers = Set() + var targetLayer: String? + + if let index = allAddedLayers.firstIndex(of: layerIdentifier) { + lowerLayers = Set(allAddedLayers.prefix(upTo: index)) + if allAddedLayers.indices.contains(index + 1) { + upperLayers = Set(allAddedLayers.suffix(from: index + 1)) + } + } + + var foundAboveLayer = false + for layerInfo in mapView.mapboxMap.allLayerIdentifiers.reversed() { + if lowerLayers.contains(layerInfo.id) { + // find the topmost layer that should be below the layerIdentifier. + if !foundAboveLayer { + layerPosition = .above(layerInfo.id) + foundAboveLayer = true + } + } else if upperLayers.contains(layerInfo.id) { + // find the bottommost layer that should be above the layerIdentifier. + layerPosition = .below(layerInfo.id) + } else if isLowermostLayer { + // find the topmost non symbol layer for layerIdentifier in lowermostSymbolLayers. + if targetLayer == nil, + layerInfo.type.rawValue != "symbol", + let sourceLayer = mapView.mapboxMap.layerProperty(for: layerInfo.id, property: "source-layer") + .value as? String, + !sourceLayer.isEmpty + { + if layerInfo.type.rawValue == "circle", + let isPersistentCircle = try? mapView.mapboxMap.isPersistentLayer(id: layerInfo.id) + { + let pitchAlignment = mapView.mapboxMap.layerProperty( + for: layerInfo.id, + property: "circle-pitch-alignment" + ).value as? String + if isPersistentCircle || (pitchAlignment != "map") { + continue + } + } + targetLayer = layerInfo.id + } + } else if isAboveRoadLayer { + // find the topmost road name label layer for layerIdentifier in arrowLayers. + if targetLayer == nil, + layerInfo.id.contains("road-label"), + mapView.mapboxMap.layerExists(withId: layerInfo.id) + { + targetLayer = layerInfo.id + } + } else { + // find the topmost layer for layerIdentifier in uppermostSymbolLayers. + if targetLayer == nil, + let sourceLayer = mapView.mapboxMap.layerProperty(for: layerInfo.id, property: "source-layer") + .value as? String, + !sourceLayer.isEmpty + { + targetLayer = layerInfo.id + } + } + } + + guard let targetLayer else { return layerPosition } + guard let layerPosition else { return .above(targetLayer) } + + if isLowermostLayer { + // For layers should be below symbol layers. + if case .below(let sequenceLayer) = layerPosition, !lowermostSymbolLayers.contains(sequenceLayer) { + // If the sequenceLayer isn't in lowermostSymbolLayers, it's above symbol layer. + // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost non symbol + // layer, + // but under the symbol layers. + return .above(targetLayer) + } + } else if isAboveRoadLayer { + // For layers should be above road name labels but below other symbol layers. + if case .below(let sequenceLayer) = layerPosition, uppermostSymbolLayers.contains(sequenceLayer) { + // If the sequenceLayer is in uppermostSymbolLayers, it's above all symbol layers. + // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost road name + // symbol layer. + return .above(targetLayer) + } else if case .above(let sequenceLayer) = layerPosition, lowermostSymbolLayers.contains(sequenceLayer) { + // If the sequenceLayer is in lowermostSymbolLayers, it's below all symbol layers. + // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost road name + // symbol layer. + return .above(targetLayer) + } + } else { + // For other layers should be uppermost and above symbol layers. + if case .above(let sequenceLayer) = layerPosition, !uppermostSymbolLayers.contains(sequenceLayer) { + // If the sequenceLayer isn't in uppermostSymbolLayers, it's below some symbol layers. + // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost layer. + return .above(targetLayer) + } + } + + return layerPosition + } +} + +extension NavigationMapStyleManager { + // TODO: These ids are specific to Standard style, we should allow customers to customize this + var poiLayerIds: [String] { + let poiLayerIds = layerIds.filter { layerId in + NavigationMapView.LayerIdentifier.clickablePoiLabels.contains { + layerId.hasPrefix($0) + } + } + return Array(poiLayerIds) + } +} From 4d5e59bc37255402a2c67a26a167fe0bb791ee7d Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sat, 8 Feb 2025 13:48:34 +0100 Subject: [PATCH 19/33] one more step ahead --- .../Other}/NavigationMapStyleManager.swift | 14 +- .../Map/Other/UIColor++.swift | 39 ++++ .../Map/Other/UIFont.swift | 7 + ...ionController+ContinuousAlternatives.swift | 1 + ...igationController+VanishingRouteLine.swift | 7 +- ios/Classes/NavigationController.swift | 178 +++++++++++++----- 6 files changed, 189 insertions(+), 57 deletions(-) rename ios/Classes/{ => MapboxNavigationCore/Map/Other}/NavigationMapStyleManager.swift (97%) create mode 100644 ios/Classes/MapboxNavigationCore/Map/Other/UIColor++.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Other/UIFont.swift diff --git a/ios/Classes/NavigationMapStyleManager.swift b/ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapStyleManager.swift similarity index 97% rename from ios/Classes/NavigationMapStyleManager.swift rename to ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapStyleManager.swift index 2dd708d2c..30601ca7f 100644 --- a/ios/Classes/NavigationMapStyleManager.swift +++ b/ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapStyleManager.swift @@ -46,7 +46,7 @@ final class NavigationMapStyleManager { private var layerIds: [String] var customizedLayerProvider: CustomizedLayerProvider = .init { $0 } - var customRouteLineLayerPosition: LayerPosition? + var customRouteLineLayerPosition: MapboxMaps.LayerPosition? private let routeFeaturesStore: MapFeaturesStore private let waypointFeaturesStore: MapFeaturesStore @@ -56,7 +56,7 @@ final class NavigationMapStyleManager { private let routeAnnotationsFeaturesStore: MapFeaturesStore private let routeAlertsFeaturesStore: MapFeaturesStore - init(mapView: MapView, customRouteLineLayerPosition: LayerPosition?) { + init(mapView: MapView, customRouteLineLayerPosition: MapboxMaps.LayerPosition?) { self.mapView = mapView self.layersOrder = Self.makeMapLayersOrder( with: mapView, @@ -160,7 +160,7 @@ final class NavigationMapStyleManager { ) } - func updateIntersectionAnnotations(routeProgress: RouteProgress) { + func updateIntersectionAnnotations(routeProgress: MapboxNavigationCore.RouteProgress) { intersectionAnnotationsFeaturesStore.update( using: routeProgress.intersectionAnnotationsMapFeatures( ids: .currentRoute, @@ -171,7 +171,7 @@ final class NavigationMapStyleManager { } func updateRouteAnnotations( - navigationRoutes: NavigationRoutes, + navigationRoutes: MapboxNavigationCore.NavigationRoutes, annotationKinds: Set, config: MapStyleConfig ) { @@ -185,7 +185,7 @@ final class NavigationMapStyleManager { } func updateRouteAlertsAnnotations( - navigationRoutes: NavigationRoutes, + navigationRoutes: MapboxNavigationCore.NavigationRoutes, excludedRouteAlertTypes: RoadAlertType, distanceTraveled: CLLocationDistance = 0.0 ) { @@ -302,7 +302,7 @@ final class NavigationMapStyleManager { private static func makeMapLayersOrder( with mapView: MapView, - customRouteLineLayerPosition: LayerPosition? + customRouteLineLayerPosition: MapboxMaps.LayerPosition? ) -> MapLayersOrder { let alternative_0_ids = FeatureIds.RouteLine.alternative(idx: 0) let alternative_1_ids = FeatureIds.RouteLine.alternative(idx: 1) @@ -375,7 +375,7 @@ final class NavigationMapStyleManager { private static func legacyLayerPosition( for layerIdentifier: String, mapView: MapView, - customRouteLineLayerPosition: LayerPosition? + customRouteLineLayerPosition: MapboxMaps.LayerPosition? ) -> MapboxMaps.LayerPosition? { let mainLineIds = FeatureIds.RouteLine.main if layerIdentifier.hasPrefix(mainLineIds.main), diff --git a/ios/Classes/MapboxNavigationCore/Map/Other/UIColor++.swift b/ios/Classes/MapboxNavigationCore/Map/Other/UIColor++.swift new file mode 100644 index 000000000..b6d60e505 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Other/UIColor++.swift @@ -0,0 +1,39 @@ +import MapboxMaps +import UIKit + +extension UIColor { + public class var defaultTintColor: UIColor { #colorLiteral(red: 0.1843137255, green: 0.4784313725, blue: 0.7764705882, alpha: 1) } + + public class var defaultRouteCasing: UIColor { .defaultTintColor } + public class var defaultRouteLayer: UIColor { #colorLiteral(red: 0.337254902, green: 0.6588235294, blue: 0.9843137255, alpha: 1) } + public class var defaultAlternateLine: UIColor { #colorLiteral(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) } + public class var defaultAlternateLineCasing: UIColor { #colorLiteral(red: 0.5019607843, green: 0.4980392157, blue: 0.5019607843, alpha: 1) } + public class var defaultManeuverArrowStroke: UIColor { .defaultRouteLayer } + public class var defaultManeuverArrow: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } + + public class var trafficUnknown: UIColor { defaultRouteLayer } + public class var trafficLow: UIColor { defaultRouteLayer } + public class var trafficModerate: UIColor { #colorLiteral(red: 1, green: 0.5843137255, blue: 0, alpha: 1) } + public class var trafficHeavy: UIColor { #colorLiteral(red: 1, green: 0.3019607843, blue: 0.3019607843, alpha: 1) } + public class var trafficSevere: UIColor { #colorLiteral(red: 0.5607843137, green: 0.1411764706, blue: 0.2784313725, alpha: 1) } + + public class var alternativeTrafficUnknown: UIColor { defaultAlternateLine } + public class var alternativeTrafficLow: UIColor { defaultAlternateLine } + public class var alternativeTrafficModerate: UIColor { #colorLiteral(red: 0.75, green: 0.63, blue: 0.53, alpha: 1.0) } + public class var alternativeTrafficHeavy: UIColor { #colorLiteral(red: 0.71, green: 0.51, blue: 0.51, alpha: 1.0) } + public class var alternativeTrafficSevere: UIColor { #colorLiteral(red: 0.71, green: 0.51, blue: 0.51, alpha: 1.0) } + + public class var defaultRouteRestrictedAreaColor: UIColor { #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) } + + public class var defaultRouteAnnotationColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } + public class var defaultSelectedRouteAnnotationColor: UIColor { #colorLiteral(red: 0.1882352941, green: 0.4470588235, blue: 0.9607843137, alpha: 1) } + + public class var defaultRouteAnnotationTextColor: UIColor { #colorLiteral(red: 0.01960784314, green: 0.02745098039, blue: 0.03921568627, alpha: 1) } + public class var defaultSelectedRouteAnnotationTextColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } + + public class var defaultRouteAnnotationMoreTimeTextColor: UIColor { #colorLiteral(red: 0.9215686275, green: 0.1450980392, blue: 0.1647058824, alpha: 1) } + public class var defaultRouteAnnotationLessTimeTextColor: UIColor { #colorLiteral(red: 0.03529411765, green: 0.6666666667, blue: 0.4549019608, alpha: 1) } + + public class var defaultWaypointColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } + public class var defaultWaypointStrokeColor: UIColor { #colorLiteral(red: 0.137254902, green: 0.1490196078, blue: 0.1764705882, alpha: 1) } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Other/UIFont.swift b/ios/Classes/MapboxNavigationCore/Map/Other/UIFont.swift new file mode 100644 index 000000000..543c67423 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Other/UIFont.swift @@ -0,0 +1,7 @@ +import UIKit + +extension UIFont { + public class var defaultRouteAnnotationTextFont: UIFont { + .systemFont(ofSize: 18) + } +} diff --git a/ios/Classes/NavigationController+ContinuousAlternatives.swift b/ios/Classes/NavigationController+ContinuousAlternatives.swift index bd39f3fa9..2896e2b1a 100644 --- a/ios/Classes/NavigationController+ContinuousAlternatives.swift +++ b/ios/Classes/NavigationController+ContinuousAlternatives.swift @@ -3,6 +3,7 @@ import Foundation import MapboxDirections import Turf import UIKit +import MapboxNavigationCore extension NavigationController { /// Returns a list of the ``AlternativeRoute``s, that are close to a certain point and are within threshold distance diff --git a/ios/Classes/NavigationController+VanishingRouteLine.swift b/ios/Classes/NavigationController+VanishingRouteLine.swift index 747f7a247..0b9e74357 100644 --- a/ios/Classes/NavigationController+VanishingRouteLine.swift +++ b/ios/Classes/NavigationController+VanishingRouteLine.swift @@ -3,6 +3,7 @@ import CoreLocation import MapboxDirections import MapboxMaps import UIKit +import MapboxNavigationCore extension NavigationController { struct RoutePoints { @@ -43,7 +44,7 @@ extension NavigationController { return RoutePoints(nestedList: nestedList, flatList: flatList) } - func updateRouteLine(routeProgress: RouteProgress) { + func updateRouteLine(routeProgress: MapboxNavigationCore.RouteProgress) { updateIntersectionAnnotations(routeProgress: routeProgress) if let routes { mapStyleManager.updateRouteAlertsAnnotations( @@ -65,12 +66,12 @@ extension NavigationController { updateArrow(routeProgress: routeProgress) } - func updateAlternatives(routeProgress: RouteProgress?) { + func updateAlternatives(routeProgress: MapboxNavigationCore.RouteProgress?) { guard let routes = routeProgress?.navigationRoutes ?? routes else { return } show(routes, routeAnnotationKinds: routeAnnotationKinds) } - func updateIntersectionAnnotations(routeProgress: RouteProgress?) { + func updateIntersectionAnnotations(routeProgress: MapboxNavigationCore.RouteProgress?) { if let routeProgress, showsIntersectionAnnotations { mapStyleManager.updateIntersectionAnnotations(routeProgress: routeProgress) } else { diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index a7c73ef9f..4c013e7f8 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -16,9 +16,9 @@ final class NavigationController: NSObject, NavigationInterface { let predictiveCacheManager: PredictiveCacheManager? @Published private(set) var isInActiveNavigation: Bool = false - @Published private(set) var currentPreviewRoutes: MapboxNavigationCore.NavigationRoutes? - @Published private(set) var activeNavigationRoutes: MapboxNavigationCore.NavigationRoutes? - @Published private(set) var currentRouteProgress: MapboxNavigationCore.RouteProgress? + //@Published private(set) var currentPreviewRoutes: MapboxNavigationCore.NavigationRoutes? + @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 @@ -29,10 +29,15 @@ final class NavigationController: NSObject, NavigationInterface { private var cancelables: Set = [] private var onNavigationListener: NavigationListener? - private let mapView: MapView - private let navigationProvider: MapboxNavigationProvider - private var navigationCamera: NavigationCamera - private let mapStyleManager: NavigationMapStyleManager + let mapView: MapView + let navigationProvider: MapboxNavigationProvider + var navigationCamera: MapboxNavigationCore.NavigationCamera + let mapStyleManager: NavigationMapStyleManager + + // Vanishing route line properties + var routePoints: RoutePoints? + var routeLineGranularDistances: RouteLineGranularDistances? + var routeRemainingDistancesIndex: Int? private var lifetimeSubscriptions: Set = [] @@ -74,7 +79,7 @@ final class NavigationController: NSObject, NavigationInterface { } core.tripSession().navigationRoutes - .assign(to: &$activeNavigationRoutes) + .assign(to: &$routes) core.navigation().locationMatching.sink { state in self.currentLocation = state.enhancedLocation @@ -102,8 +107,8 @@ final class NavigationController: NSObject, NavigationInterface { private var customRouteLineLayerPosition: MapboxMaps.LayerPosition? = nil { didSet { mapStyleManager.customRouteLineLayerPosition = customRouteLineLayerPosition - guard let activeNavigationRoutes else { return } - show(activeNavigationRoutes, routeAnnotationKinds: routeAnnotationKinds) + guard let routes else { return } + show(routes, routeAnnotationKinds: routeAnnotationKinds) } } @@ -116,15 +121,11 @@ final class NavigationController: NSObject, NavigationInterface { didSet { updateCameraPadding() } } - @_spi(MapboxInternal) public var showsViewportDebugView: Bool = false { - didSet { updateDebugViewportVisibility() } - } - /// Controls whether to show annotations on intersections, e.g. traffic signals, railroad crossings, yield and stop /// signs. Defaults to `true`. public var showsIntersectionAnnotations: Bool = true { didSet { - updateIntersectionAnnotations(routeProgress: currentRouteProgress) + updateIntersectionAnnotations(routeProgress: routeProgress) } } @@ -132,7 +133,7 @@ final class NavigationController: NSObject, NavigationInterface { /// Defaults to `true`. public var showsAlternatives: Bool = true { didSet { - updateAlternatives(routeProgress: currentRouteProgress) + updateAlternatives(routeProgress: routeProgress) } } @@ -145,7 +146,7 @@ final class NavigationController: NSObject, NavigationInterface { } else { routeAnnotationKinds.removeAll() } - updateAlternatives(routeProgress: currentRouteProgress) + updateAlternatives(routeProgress: routeProgress) } } @@ -185,11 +186,94 @@ final class NavigationController: NSObject, NavigationInterface { } } + // MARK: RouteLine Customization + + /// Configures the route line color for the main route. + /// If set, overrides the `.unknown` and `.low` traffic colors. + @objc public dynamic var routeColor: UIColor { + get { + congestionConfiguration.colors.mainRouteColors.unknown + } + set { + congestionConfiguration.colors.mainRouteColors.unknown = newValue + congestionConfiguration.colors.mainRouteColors.low = newValue + } + } + + /// Configures the route line color for alternative routes. + /// If set, overrides the `.unknown` and `.low` traffic colors. + @objc public dynamic var routeAlternateColor: UIColor { + get { + congestionConfiguration.colors.alternativeRouteColors.unknown + } + set { + congestionConfiguration.colors.alternativeRouteColors.unknown = newValue + congestionConfiguration.colors.alternativeRouteColors.low = newValue + } + } + + /// Configures the casing route line color for the main route. + @objc public dynamic var routeCasingColor: UIColor = .defaultRouteCasing + /// Configures the casing route line color for alternative routes. + @objc public dynamic var routeAlternateCasingColor: UIColor = .defaultAlternateLineCasing + /// Configures the color for restricted areas on the route line. + @objc public dynamic var routeRestrictedAreaColor: UIColor = .defaultRouteRestrictedAreaColor + /// Configures the color for the traversed part of the main route. The traversed part is rendered only if the color + /// is not `nil`. + /// Defaults to `nil`. + @objc public dynamic var traversedRouteColor: UIColor? = nil + /// Configures the color of the maneuver arrow. + @objc public dynamic var maneuverArrowColor: UIColor = .defaultManeuverArrow + /// Configures the stroke color of the maneuver arrow. + @objc public dynamic var maneuverArrowStrokeColor: UIColor = .defaultManeuverArrowStroke + + // MARK: Route Annotations Customization + + /// Configures the color of the route annotation for the main route. + @objc public dynamic var routeAnnotationSelectedColor: UIColor = + .defaultSelectedRouteAnnotationColor + /// Configures the color of the route annotation for alternative routes. + @objc public dynamic var routeAnnotationColor: UIColor = .defaultRouteAnnotationColor + /// Configures the text color of the route annotation for the main route. + @objc public dynamic var routeAnnotationSelectedTextColor: UIColor = .defaultSelectedRouteAnnotationTextColor + /// Configures the text color of the route annotation for alternative routes. + @objc public dynamic var routeAnnotationTextColor: UIColor = .defaultRouteAnnotationTextColor + /// Configures the text color of the route annotation for alternative routes when relative duration is greater then + /// the main route. + @objc public dynamic var routeAnnotationMoreTimeTextColor: UIColor = .defaultRouteAnnotationMoreTimeTextColor + /// Configures the text color of the route annotation for alternative routes when relative duration is lesser then + /// the main route. + @objc public dynamic var routeAnnotationLessTimeTextColor: UIColor = .defaultRouteAnnotationLessTimeTextColor + /// Configures the text font of the route annotations. + @objc public dynamic var routeAnnotationTextFont: UIFont = .defaultRouteAnnotationTextFont + /// Configures the waypoint color. + @objc public dynamic var waypointColor: UIColor = .defaultWaypointColor + /// Configures the waypoint stroke color. + @objc public dynamic var waypointStrokeColor: UIColor = .defaultWaypointStrokeColor + public func update(navigationCameraState: MapboxNavigationCore.NavigationCameraState) { guard cameraState != navigationCamera.currentCameraState else { return } navigationCamera.update(cameraState: navigationCameraState) } + /// Represents a set of ``RoadAlertType`` values that should be hidden from the map display. + /// By default, this is an empty set, which indicates that all road alerts will be displayed. + /// + /// - Note: If specific `RoadAlertType` values are added to this set, those alerts will be + /// excluded from the map rendering. + public var excludedRouteAlertTypes: RoadAlertType = [] { + didSet { + guard let navigationRoutes = routes else { + return + } + + mapStyleManager.updateRouteAlertsAnnotations( + navigationRoutes: navigationRoutes, + excludedRouteAlertTypes: excludedRouteAlertTypes + ) + } + } + /// Visualizes the given routes and it's alternatives, removing any existing from the map. /// /// Each route is visualized as a line. Each line is color-coded by traffic congestion, if congestion @@ -205,7 +289,7 @@ final class NavigationController: NSObject, NavigationInterface { routeAnnotationKinds: Set ) { removeRoutes() - activeNavigationRoutes = navigationRoutes + routes = navigationRoutes self.routeAnnotationKinds = routeAnnotationKinds let mainRoute = navigationRoutes.mainRoute.route if routeLineTracksTraversal { @@ -231,13 +315,13 @@ final class NavigationController: NSObject, NavigationInterface { /// Removes routes and all visible annotations from the map. public func removeRoutes() { - activeNavigationRoutes = nil + routes = nil routeLineGranularDistances = nil routeRemainingDistancesIndex = nil mapStyleManager.removeAllFeatures() } - func updateArrow(routeProgress: RouteProgress) { + func updateArrow(routeProgress: MapboxNavigationCore.RouteProgress) { if routeProgress.currentLegProgress.followOnStep != nil { mapStyleManager.updateArrows( route: routeProgress.route, @@ -255,34 +339,11 @@ final class NavigationController: NSObject, NavigationInterface { mapStyleManager.removeArrows() } - // MARK: - Debug Viewport - - private func updateDebugViewportVisibility() { - if showsViewportDebugView { - let viewportDebugView = with(UIView(frame: .zero)) { - $0.layer.borderWidth = 1 - $0.layer.borderColor = UIColor.blue.cgColor - $0.backgroundColor = .clear - } - addSubview(viewportDebugView) - self.viewportDebugView = viewportDebugView - viewportDebugView.isUserInteractionEnabled = false - updateViewportDebugView() - } else { - viewportDebugView?.removeFromSuperview() - viewportDebugView = nil - } - } - - private func updateViewportDebugView() { - viewportDebugView?.frame = bounds.inset(by: navigationCamera.viewportPadding) - } - // MARK: - Camera private func updateCameraPadding() { let padding = viewportPadding - let safeAreaInsets = safeAreaInsets + let safeAreaInsets = mapView.safeAreaInsets navigationCamera.viewportPadding = .init( top: safeAreaInsets.top + padding.top, @@ -290,7 +351,6 @@ final class NavigationController: NSObject, NavigationInterface { bottom: safeAreaInsets.bottom + padding.bottom, right: safeAreaInsets.right + padding.right ) - updateViewportDebugView() } private func fitCamera( @@ -327,6 +387,30 @@ final class NavigationController: NSObject, NavigationInterface { } } + private var customRouteLineFeatureProvider: RouteLineFeatureProvider { + .init { [weak self] identifier, sourceIdentifier in + guard let self else { return nil } + return delegate?.navigationMapView( + self, + routeLineLayerWithIdentifier: identifier, + sourceIdentifier: sourceIdentifier + ) + } customRouteCasingLineLayer: { [weak self] identifier, sourceIdentifier in + guard let self else { return nil } + return delegate?.navigationMapView( + self, + routeCasingLineLayerWithIdentifier: identifier, + sourceIdentifier: sourceIdentifier + ) + } customRouteRestrictedAreasLineLayer: { [weak self] identifier, sourceIdentifier in + guard let self else { return nil } + return delegate?.navigationMapView( + self, + routeRestrictedAreasLineLayerWithIdentifier: identifier, + sourceIdentifier: sourceIdentifier + ) + } + } private var waypointsFeatureProvider: WaypointFeatureProvider { .init { [weak self] waypoints, legIndex in @@ -350,14 +434,14 @@ final class NavigationController: NSObject, NavigationInterface { } private func updateWaypointsVisiblity() { - guard let mainRoute = routes?.mainRoute.route else { + guard let mainRoute = routes?.mainRoute.route else { mapStyleManager.removeWaypoints() return } mapStyleManager.updateWaypoints( route: mainRoute, - legIndex: currentRouteProgress?.legIndex ?? 0, + legIndex: routeProgress?.legIndex ?? 0, config: mapStyleConfig, featureProvider: waypointsFeatureProvider ) From c9b9d023224feb2f351e98ced4c2436d3223c584 Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sat, 8 Feb 2025 20:50:47 +0100 Subject: [PATCH 20/33] more progress --- .../Map/Other/NavigationMapIdentifiers.swift | 37 ++ .../Style/AlternativeRoute+Deviation.swift | 29 ++ .../Map/Style/FeatureIds.swift | 169 ++++++++ .../IntersectionAnnotationsMapFeatures.swift | 142 ++++++ .../Map/Style/ManeuverArrowMapFeatures.swift | 142 ++++++ .../Map/Style/MapFeatures/ETAView.swift | 285 ++++++++++++ .../ETAViewsAnnotationFeature.swift | 139 ++++++ .../Style/MapFeatures/GeoJsonMapFeature.swift | 239 +++++++++++ .../Map/Style/MapFeatures/MapFeature.swift | 16 + .../Style/MapFeatures/MapFeaturesStore.swift | 117 +++++ .../Map/Style/MapFeatures/Style++.swift | 38 ++ .../Map/Style/MapLayersOrder.swift | 260 +++++++++++ .../NavigationMapStyleManager.swift | 0 .../RouteAlertsAnnotationsMapFeatures.swift | 373 ++++++++++++++++ .../Style/RouteAnnotationMapFeatures.swift | 55 +++ .../Map/Style/RouteLineMapFeatures.swift | 406 ++++++++++++++++++ .../Style/VoiceInstructionsMapFeatures.swift | 67 +++ .../Map/Style/WaypointsMapFeature.swift | 157 +++++++ .../NavigationController+Gestures.swift | 41 +- ios/Classes/NavigationController.swift | 23 +- 20 files changed, 2708 insertions(+), 27 deletions(-) create mode 100644 ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/FeatureIds.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapLayersOrder.swift rename ios/Classes/MapboxNavigationCore/Map/{Other => Style}/NavigationMapStyleManager.swift (100%) create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift create mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift diff --git a/ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift b/ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift new file mode 100644 index 000000000..17f7a2b57 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift @@ -0,0 +1,37 @@ +import Foundation + +extension NavigationController { + static let identifier = "com.mapbox.navigation.core" + + @MainActor + enum LayerIdentifier { + static let puck2DLayer: String = "puck" + static let puck3DLayer: String = "puck-model-layer" + static let poiLabelLayer: String = "poi-label" + static let transitLabelLayer: String = "transit-label" + static let airportLabelLayer: String = "airport-label" + + static var clickablePoiLabels: [String] { + [ + LayerIdentifier.poiLabelLayer, + LayerIdentifier.transitLabelLayer, + LayerIdentifier.airportLabelLayer, + ] + } + } + + enum ImageIdentifier { + static let markerImage = "default_marker" + static let midpointMarkerImage = "midpoint_marker" + static let trafficSignal = "traffic_signal" + static let railroadCrossing = "railroad_crossing" + static let yieldSign = "yield_sign" + static let stopSign = "stop_sign" + static let searchAnnotationImage = "search_annotation" + static let selectedSearchAnnotationImage = "search_annotation_selected" + } + + enum ModelKeyIdentifier { + static let modelSouce = "puck-model" + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift b/ios/Classes/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift new file mode 100644 index 000000000..172b9b1b0 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift @@ -0,0 +1,29 @@ +import Foundation + +extension AlternativeRoute { + /// Returns offset of the alternative route where it deviates from the main route. + func deviationOffset() -> Double { + guard let coordinates = route.shape?.coordinates, + !coordinates.isEmpty + else { + return 0 + } + + let splitGeometryIndex = alternativeRouteIntersectionIndices.routeGeometryIndex + + var totalDistance = 0.0 + var pointDistance: Double? = nil + for index in stride(from: coordinates.count - 1, to: 0, by: -1) { + let currCoordinate = coordinates[index] + let prevCoordinate = coordinates[index - 1] + totalDistance += currCoordinate.projectedDistance(to: prevCoordinate) + + if index == splitGeometryIndex + 1 { + pointDistance = totalDistance + } + } + guard let pointDistance, totalDistance != 0 else { return 0 } + + return (totalDistance - pointDistance) / totalDistance + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/FeatureIds.swift b/ios/Classes/MapboxNavigationCore/Map/Style/FeatureIds.swift new file mode 100644 index 000000000..994ef0f96 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/FeatureIds.swift @@ -0,0 +1,169 @@ + +enum FeatureIds { + private static let globalPrefix: String = "com.mapbox.navigation" + + struct RouteLine: Hashable, Sendable { + private static let prefix: String = "\(globalPrefix).route_line" + + static var main: Self { + .init(routeId: "\(prefix).main") + } + + static func alternative(idx: Int) -> Self { + .init(routeId: "\(prefix).alternative_\(idx)") + } + + let source: String + let main: String + let casing: String + + let restrictedArea: String + let restrictedAreaSource: String + let traversedRoute: String + + init(routeId: String) { + self.source = routeId + self.main = routeId + self.casing = "\(routeId).casing" + self.restrictedArea = "\(routeId).restricted_area" + self.restrictedAreaSource = "\(routeId).restricted_area" + self.traversedRoute = "\(routeId).traversed_route" + } + } + + struct ManeuverArrow { + private static let prefix: String = "\(globalPrefix).arrow" + + let id: String + let symbolId: String + let arrow: String + let arrowStroke: String + let arrowSymbol: String + let arrowSymbolCasing: String + let arrowSource: String + let arrowSymbolSource: String + let triangleTipImage: String + + init(arrowId: String) { + let id = "\(Self.prefix).\(arrowId)" + self.id = id + self.symbolId = "\(id).symbol" + self.arrow = "\(id)" + self.arrowStroke = "\(id).stroke" + self.arrowSymbol = "\(id).symbol" + self.arrowSymbolCasing = "\(id).symbol.casing" + self.arrowSource = "\(id).source" + self.arrowSymbolSource = "\(id).symbol_source" + self.triangleTipImage = "\(id).triangle_tip_image" + } + + static func nextArrow() -> Self { + .init(arrowId: "next") + } + } + + struct VoiceInstruction { + private static let prefix: String = "\(globalPrefix).voice_instruction" + + let featureId: String + let source: String + let layer: String + let circleLayer: String + + init() { + let id = "\(Self.prefix)" + self.featureId = id + self.source = "\(id).source" + self.layer = "\(id).layer" + self.circleLayer = "\(id).layer.circle" + } + + static var currentRoute: Self { + .init() + } + } + + struct IntersectionAnnotation { + private static let prefix: String = "\(globalPrefix).intersection_annotations" + + let featureId: String + let source: String + let layer: String + + let yieldSignImage: String + let stopSignImage: String + let railroadCrossingImage: String + let trafficSignalImage: String + + init() { + let id = "\(Self.prefix)" + self.featureId = id + self.source = "\(id).source" + self.layer = "\(id).layer" + self.yieldSignImage = "\(id).yield_sign" + self.stopSignImage = "\(id).stop_sign" + self.railroadCrossingImage = "\(id).railroad_crossing" + self.trafficSignalImage = "\(id).traffic_signal" + } + + static var currentRoute: Self { + .init() + } + } + + struct RouteAlertAnnotation { + private static let prefix: String = "\(globalPrefix).route_alert_annotations" + + let featureId: String + let source: String + let layer: String + + init() { + let id = "\(Self.prefix)" + self.featureId = id + self.source = "\(id).source" + self.layer = "\(id).layer" + } + + static var `default`: Self { + .init() + } + } + + struct RouteWaypoints { + private static let prefix: String = "\(globalPrefix)_waypoint" + + let featureId: String + let innerCircle: String + let markerIcon: String + let source: String + + init() { + self.featureId = "\(Self.prefix).route-waypoints" + self.innerCircle = "\(Self.prefix).innerCircleLayer" + self.markerIcon = "\(Self.prefix).symbolLayer" + self.source = "\(Self.prefix).source" + } + + static var `default`: Self { + .init() + } + } + + struct RouteAnnotation: Hashable, Sendable { + private static let prefix: String = "\(globalPrefix).route_line.annotation" + let layerId: String + + static var main: Self { + .init(annotationId: "\(prefix).main") + } + + static func alternative(index: Int) -> Self { + .init(annotationId: "\(prefix).alternative_\(index)") + } + + init(annotationId: String) { + self.layerId = annotationId + } + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift new file mode 100644 index 000000000..559a02d05 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift @@ -0,0 +1,142 @@ +import _MapboxNavigationHelpers +import MapboxDirections +import MapboxMaps +import enum SwiftUI.ColorScheme +import MapboxNavigationCore + +extension RouteProgress { + func intersectionAnnotationsMapFeatures( + ids: FeatureIds.IntersectionAnnotation, + customizedLayerProvider: CustomizedLayerProvider + ) -> [any MapFeature] { + guard !routeIsComplete else { + return [] + } + + var featureCollection = FeatureCollection(features: []) + + let stepProgress = currentLegProgress.currentStepProgress + let intersectionIndex = stepProgress.intersectionIndex + let intersections = stepProgress.intersectionsIncludingUpcomingManeuverIntersection ?? [] + let stepIntersections = Array(intersections.dropFirst(intersectionIndex)) + + for intersection in stepIntersections { + if let feature = intersectionFeature(from: intersection, ids: ids) { + featureCollection.features.append(feature) + } + } + + let layers: [any Layer] = [ + with(SymbolLayer(id: ids.layer, source: ids.source)) { + $0.iconAllowOverlap = .constant(false) + $0.iconImage = .expression(Exp(.get) { + "imageName" + }) + }, + ] + return [ + GeoJsonMapFeature( + id: ids.featureId, + sources: [ + .init( + id: ids.source, + geoJson: .featureCollection(featureCollection) + ), + ], + customizeSource: { _, _ in }, + layers: layers.map { customizedLayerProvider.customizedLayer($0) }, + onBeforeAdd: { mapView in + Self.upsertIntersectionSymbolImages( + map: mapView.mapboxMap, + ids: ids + ) + }, + onUpdate: { mapView in + Self.upsertIntersectionSymbolImages( + map: mapView.mapboxMap, + ids: ids + ) + }, + onAfterRemove: { mapView in + do { + try Self.removeIntersectionSymbolImages( + map: mapView.mapboxMap, + ids: ids + ) + } catch { + Log.error( + "Failed to remove intersection annotation images with error \(error)", + category: .navigationUI + ) + } + } + ), + ] + } + + private func intersectionFeature( + from intersection: Intersection, + ids: FeatureIds.IntersectionAnnotation + ) -> Feature? { + var properties: JSONObject? + if intersection.yieldSign == true { + properties = ["imageName": .string(ids.yieldSignImage)] + } + if intersection.stopSign == true { + properties = ["imageName": .string(ids.stopSignImage)] + } + if intersection.railroadCrossing == true { + properties = ["imageName": .string(ids.railroadCrossingImage)] + } + if intersection.trafficSignal == true { + properties = ["imageName": .string(ids.trafficSignalImage)] + } + + guard let properties else { return nil } + + var feature = Feature(geometry: .point(Point(intersection.location))) + feature.properties = properties + return feature + } + + public static func resourceBundle() -> Bundle? { + let bundle = Bundle(for: MapboxNavigationProvider.self) + if let resourceBundleURL = bundle.url(forResource: "MapboxNavigationCoreResources", withExtension: "bundle") { + return Bundle(url: resourceBundleURL) + } + return nil + } + + private static func upsertIntersectionSymbolImages( + map: MapboxMap, + ids: FeatureIds.IntersectionAnnotation + ) { + for (imageName, imageIdentifier) in imageNameToMapIdentifier(ids: ids) { + if let image = resourceBundle()?.image(named: imageName) { + map.provisionImage(id: imageIdentifier) { style in + try style.addImage(image, id: imageIdentifier) + } + } + } + } + + private static func removeIntersectionSymbolImages( + map: MapboxMap, + ids: FeatureIds.IntersectionAnnotation + ) throws { + for (_, imageIdentifier) in imageNameToMapIdentifier(ids: ids) { + try map.removeImage(withId: imageIdentifier) + } + } + + private static func imageNameToMapIdentifier( + ids: FeatureIds.IntersectionAnnotation + ) -> [String: String] { + return [ + "TrafficSignal": ids.trafficSignalImage, + "RailroadCrossing": ids.railroadCrossingImage, + "YieldSign": ids.yieldSignImage, + "StopSign": ids.stopSignImage, + ] + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift new file mode 100644 index 000000000..ac4d79bbb --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift @@ -0,0 +1,142 @@ +import _MapboxNavigationHelpers +import Foundation +import MapboxDirections +@_spi(Experimental) import MapboxMaps + +extension Route { + + func maneuverArrowMapFeatures( + ids: FeatureIds.ManeuverArrow, + cameraZoom: CGFloat, + legIndex: Int, stepIndex: Int, + config: MapStyleConfig, + customizedLayerProvider: CustomizedLayerProvider + ) -> [any MapFeature] { + guard containsStep(at: legIndex, stepIndex: stepIndex) + else { return [] } + + let bundle = Bundle(for: MapboxNavigationProvider.self) + var moduleBundle: Bundle? = nil + if let resourceBundleURL = bundle.url(forResource: "MapboxNavigationCoreResources", withExtension: "bundle") { + moduleBundle = Bundle(url: resourceBundleURL) + } + + if moduleBundle == nil { + return [] + } + + let triangleImage = moduleBundle!.image(named: "triangle")!.withRenderingMode(.alwaysTemplate) + + var mapFeatures: [any MapFeature] = [] + + let step = legs[legIndex].steps[stepIndex] + let maneuverCoordinate = step.maneuverLocation + guard step.maneuverType != .arrive else { return [] } + + let metersPerPoint = Projection.metersPerPoint( + for: maneuverCoordinate.latitude, + zoom: cameraZoom + ) + + // TODO: Implement ability to change `shaftLength` depending on zoom level. + let shaftLength = max(min(50 * metersPerPoint, 50), 30) + let shaftPolyline = polylineAroundManeuver(legIndex: legIndex, stepIndex: stepIndex, distance: shaftLength) + + if shaftPolyline.coordinates.count > 1 { + let minimumZoomLevel = 14.5 + let shaftStrokeCoordinates = shaftPolyline.coordinates + let shaftDirection = shaftStrokeCoordinates[shaftStrokeCoordinates.count - 2] + .direction(to: shaftStrokeCoordinates.last!) + let point = Point(shaftStrokeCoordinates.last!) + + let layers: [any Layer] = [ + with(LineLayer(id: ids.arrow, source: ids.arrowSource)) { + $0.minZoom = Double(minimumZoomLevel) + $0.lineCap = .constant(.butt) + $0.lineJoin = .constant(.round) + $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.70)) + $0.lineColor = .constant(.init(config.maneuverArrowColor)) + $0.lineEmissiveStrength = .constant(1) + }, + with(LineLayer(id: ids.arrowStroke, source: ids.arrowSource)) { + $0.minZoom = Double(minimumZoomLevel) + $0.lineCap = .constant(.butt) + $0.lineJoin = .constant(.round) + $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.80)) + $0.lineColor = .constant(.init(config.maneuverArrowStrokeColor)) + $0.lineEmissiveStrength = .constant(1) + }, + with(SymbolLayer(id: ids.arrowSymbol, source: ids.arrowSymbolSource)) { + $0.minZoom = Double(minimumZoomLevel) + $0.iconImage = .constant(.name(ids.triangleTipImage)) + $0.iconColor = .constant(.init(config.maneuverArrowColor)) + $0.iconRotationAlignment = .constant(.map) + $0.iconRotate = .constant(.init(shaftDirection)) + $0.iconSize = .expression(Expression.routeLineWidthExpression(0.12)) + $0.iconAllowOverlap = .constant(true) + $0.iconEmissiveStrength = .constant(1) + }, + with(SymbolLayer(id: ids.arrowSymbolCasing, source: ids.arrowSymbolSource)) { + $0.minZoom = Double(minimumZoomLevel) + $0.iconImage = .constant(.name(ids.triangleTipImage)) + $0.iconColor = .constant(.init(config.maneuverArrowStrokeColor)) + $0.iconRotationAlignment = .constant(.map) + $0.iconRotate = .constant(.init(shaftDirection)) + $0.iconSize = .expression(Expression.routeLineWidthExpression(0.14)) + $0.iconAllowOverlap = .constant(true) + }, + ] + + mapFeatures.append( + GeoJsonMapFeature( + id: ids.id, + sources: [ + .init( + id: ids.arrowSource, + geoJson: .feature(Feature(geometry: .lineString(shaftPolyline))) + ), + .init( + id: ids.arrowSymbolSource, + geoJson: .feature(Feature(geometry: .point(point))) + ), + ], + customizeSource: { source, _ in + source.tolerance = 0.375 + }, + layers: layers.map { customizedLayerProvider.customizedLayer($0) }, + onBeforeAdd: { mapView in + mapView.mapboxMap.provisionImage(id: ids.triangleTipImage) { + try $0.addImage( + triangleImage, + id: ids.triangleTipImage, + sdf: true, + stretchX: [], + stretchY: [] + ) + } + }, + onUpdate: { mapView in + try with(mapView.mapboxMap) { + try $0.setLayerProperty( + for: ids.arrowSymbol, + property: "icon-rotate", + value: shaftDirection + ) + try $0.setLayerProperty( + for: ids.arrowSymbolCasing, + property: "icon-rotate", + value: shaftDirection + ) + } + }, + onAfterRemove: { mapView in + if mapView.mapboxMap.imageExists(withId: ids.triangleTipImage) { + try? mapView.mapboxMap.removeImage(withId: ids.triangleTipImage) + } + } + ) + ) + } + return mapFeatures + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift new file mode 100644 index 000000000..40063dbd1 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift @@ -0,0 +1,285 @@ +import MapboxMaps +import UIKit + +final class ETAView: UIView { + private let label = { + let label = UILabel() + label.textAlignment = .left + return label + }() + + private var tail = UIView() + private let backgroundShape = CAShapeLayer() + private let mapStyleConfig: MapStyleConfig + + let textColor: UIColor + let baloonColor: UIColor + var padding = UIEdgeInsets(allEdges: 10) + var tailSize = 8.0 + var cornerRadius = 8.0 + + var text: String { + didSet { update() } + } + + var anchor: ViewAnnotationAnchor? { + didSet { setNeedsLayout() } + } + + convenience init( + eta: TimeInterval, + isSelected: Bool, + tollsHint: Bool?, + mapStyleConfig: MapStyleConfig + ) { + let viewLabel = DateComponentsFormatter.travelTimeString(eta, signed: false) + + let textColor: UIColor + let baloonColor: UIColor + if isSelected { + textColor = mapStyleConfig.routeAnnotationSelectedTextColor + baloonColor = mapStyleConfig.routeAnnotationSelectedColor + } else { + textColor = mapStyleConfig.routeAnnotationTextColor + baloonColor = mapStyleConfig.routeAnnotationColor + } + + self.init( + text: viewLabel, + tollsHint: tollsHint, + mapStyleConfig: mapStyleConfig, + textColor: textColor, + baloonColor: baloonColor + ) + } + + convenience init( + travelTimeDelta: TimeInterval, + tollsHint: Bool?, + mapStyleConfig: MapStyleConfig + ) { + let textColor: UIColor + let timeDelta: String + if abs(travelTimeDelta) >= 180 { + textColor = if travelTimeDelta > 0 { + mapStyleConfig.routeAnnotationMoreTimeTextColor + } else { + mapStyleConfig.routeAnnotationLessTimeTextColor + } + timeDelta = DateComponentsFormatter.travelTimeString( + travelTimeDelta, + signed: true + ) + } else { + textColor = mapStyleConfig.routeAnnotationTextColor + timeDelta = "SAME_TIME".localizedString( + value: "Similar ETA", + comment: "Alternatives selection note about equal travel time." + ) + } + + self.init( + text: timeDelta, + tollsHint: tollsHint, + mapStyleConfig: mapStyleConfig, + textColor: textColor, + baloonColor: mapStyleConfig.routeAnnotationColor + ) + } + + init( + text: String, + tollsHint: Bool?, + mapStyleConfig: MapStyleConfig, + textColor: UIColor = .darkText, + baloonColor: UIColor = .white + ) { + var viewLabel = text + switch tollsHint { + case .none: + label.numberOfLines = 1 + case .some(true): + label.numberOfLines = 2 + viewLabel += "\n" + "ROUTE_HAS_TOLLS".localizedString( + value: "Tolls", + comment: "Route callout label, indicating there are tolls on the route.") + if let symbol = Locale.current.currencySymbol { + viewLabel += " " + symbol + } + case .some(false): + label.numberOfLines = 2 + viewLabel += "\n" + "ROUTE_HAS_NO_TOLLS".localizedString( + value: "No Tolls", + comment: "Route callout label, indicating there are no tolls on the route.") + } + + self.text = viewLabel + self.textColor = textColor + self.baloonColor = baloonColor + self.mapStyleConfig = mapStyleConfig + super.init(frame: .zero) + layer.addSublayer(backgroundShape) + backgroundShape.shadowRadius = 1.4 + backgroundShape.shadowOffset = CGSize(width: 0, height: 0.7) + backgroundShape.shadowColor = UIColor.black.cgColor + backgroundShape.shadowOpacity = 0.3 + + addSubview(label) + + update() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var attributedText: NSAttributedString { + let text = NSMutableAttributedString( + attributedString: .labelText( + text, + font: mapStyleConfig.routeAnnotationTextFont, + color: textColor + ) + ) + return text + } + + private func update() { + backgroundShape.fillColor = baloonColor.cgColor + label.attributedText = attributedText + } + + struct Layout { + var label: CGRect + var bubble: CGRect + var size: CGSize + + init(availableSize: CGSize, text: NSAttributedString, tailSize: CGFloat, padding: UIEdgeInsets) { + let tailPadding = UIEdgeInsets(allEdges: tailSize) + + let textPadding = padding + tailPadding + UIEdgeInsets.zero + let textAvailableSize = availableSize - textPadding + let textSize = text.boundingRect( + with: textAvailableSize, + options: .usesLineFragmentOrigin, context: nil + ).size.roundedUp() + self.label = CGRect(padding: textPadding, size: textSize) + self.bubble = CGRect(padding: tailPadding, size: textSize + textPadding - tailPadding) + self.size = bubble.size + tailPadding + } + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + Layout(availableSize: size, text: attributedText, tailSize: tailSize, padding: padding).size + } + + override func layoutSubviews() { + super.layoutSubviews() + + let layout = Layout(availableSize: bounds.size, text: attributedText, tailSize: tailSize, padding: padding) + label.frame = layout.label + + let calloutPath = UIBezierPath.calloutPath( + size: bounds.size, + tailSize: tailSize, + cornerRadius: cornerRadius, + anchor: anchor ?? .center + ) + backgroundShape.path = calloutPath.cgPath + backgroundShape.frame = bounds + } +} + +extension UIEdgeInsets { + fileprivate init(allEdges value: CGFloat) { + self.init(top: value, left: value, bottom: value, right: value) + } +} + +extension NSAttributedString { + fileprivate static func labelText(_ string: String, font: UIFont, color: UIColor) -> NSAttributedString { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .left + let attributes = [ + NSAttributedString.Key.paragraphStyle: paragraphStyle, + .font: font, + .foregroundColor: color, + ] + return NSAttributedString(string: string, attributes: attributes) + } +} + +extension CGSize { + fileprivate func roundedUp() -> CGSize { + CGSize(width: width.rounded(.up), height: height.rounded(.up)) + } +} + +extension CGRect { + fileprivate init(padding: UIEdgeInsets, size: CGSize) { + self.init(origin: CGPoint(x: padding.left, y: padding.top), size: size) + } +} + +private func + (lhs: CGSize, rhs: UIEdgeInsets) -> CGSize { + return CGSize(width: lhs.width + rhs.left + rhs.right, height: lhs.height + rhs.top + rhs.bottom) +} + +private func - (lhs: CGSize, rhs: UIEdgeInsets) -> CGSize { + return CGSize(width: lhs.width - rhs.left - rhs.right, height: lhs.height - rhs.top - rhs.bottom) +} + +extension UIBezierPath { + fileprivate static func calloutPath( + size: CGSize, + tailSize: CGFloat, + cornerRadius: CGFloat, + anchor: ViewAnnotationAnchor + ) -> UIBezierPath { + let rect = CGRect(origin: .init(x: 0, y: 0), size: size) + let bubbleRect = rect.insetBy(dx: tailSize, dy: tailSize) + + let path = UIBezierPath( + roundedRect: bubbleRect, + cornerRadius: cornerRadius + ) + + let tailPath = UIBezierPath() + let p = tailSize + let h = size.height + let w = size.width + let r = cornerRadius + let tailPoints: [CGPoint] = switch anchor { + case .topLeft: + [CGPoint(x: 0, y: 0), CGPoint(x: p + r, y: p), CGPoint(x: p, y: p + r)] + case .top: + [CGPoint(x: w / 2, y: 0), CGPoint(x: w / 2 - p, y: p), CGPoint(x: w / 2 + p, y: p)] + case .topRight: + [CGPoint(x: w, y: 0), CGPoint(x: w - p, y: p + r), CGPoint(x: w - 3 * p, y: p)] + case .bottomLeft: + [CGPoint(x: 0, y: h), CGPoint(x: p, y: h - (p + r)), CGPoint(x: p + r, y: h - p)] + case .bottom: + [CGPoint(x: w / 2, y: h), CGPoint(x: w / 2 - p, y: h - p), CGPoint(x: w / 2 + p, y: h - p)] + case .bottomRight: + [CGPoint(x: w, y: h), CGPoint(x: w - (p + r), y: h - p), CGPoint(x: w - p, y: h - (p + r))] + case .left: + [CGPoint(x: 0, y: h / 2), CGPoint(x: p, y: h / 2 - p), CGPoint(x: p, y: h / 2 + p)] + case .right: + [CGPoint(x: w, y: h / 2), CGPoint(x: w - p, y: h / 2 - p), CGPoint(x: w - p, y: h / 2 + p)] + default: + [] + } + + for (i, point) in tailPoints.enumerated() { + if i == 0 { + tailPath.move(to: point) + } else { + tailPath.addLine(to: point) + } + } + tailPath.close() + path.append(tailPath) + return path + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift new file mode 100644 index 000000000..75d9b5c17 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift @@ -0,0 +1,139 @@ +import Foundation +import MapboxDirections +import MapboxMaps + +struct ETAViewsAnnotationFeature: MapFeature { + var id: String + + private let viewAnnotations: [ViewAnnotation] + + init( + for navigationRoutes: NavigationRoutes, + showMainRoute: Bool, + showAlternatives: Bool, + isRelative: Bool, + annotateAtManeuver: Bool, + mapStyleConfig: MapStyleConfig + ) { + let routesContainTolls = navigationRoutes.alternativeRoutes.contains { + ($0.route.tollIntersections?.count ?? 0) > 0 + } + var featureId = "" + + var annotations = [ViewAnnotation]() + if showMainRoute { + featureId += navigationRoutes.mainRoute.routeId.rawValue + let tollsHint = routesContainTolls ? navigationRoutes.mainRoute.route.containsTolls : nil + let etaView = ETAView( + eta: navigationRoutes.mainRoute.route.expectedTravelTime, + isSelected: true, + tollsHint: tollsHint, + mapStyleConfig: mapStyleConfig + ) + if let geometry = navigationRoutes.mainRoute.route.geometryForCallout() { + annotations.append( + ViewAnnotation( + annotatedFeature: .geometry(geometry), + view: etaView + ) + ) + } else { + annotations.append( + ViewAnnotation( + layerId: FeatureIds.RouteAnnotation.main.layerId, + view: etaView + ) + ) + } + } + if showAlternatives { + for (idx, alternativeRoute) in navigationRoutes.alternativeRoutes.enumerated() { + featureId += alternativeRoute.routeId.rawValue + let tollsHint = routesContainTolls ? alternativeRoute.route.containsTolls : nil + let etaView = if isRelative { + ETAView( + travelTimeDelta: alternativeRoute.expectedTravelTimeDelta, + tollsHint: tollsHint, + mapStyleConfig: mapStyleConfig + ) + } else { + ETAView( + eta: alternativeRoute.infoFromOrigin.duration, + isSelected: false, + tollsHint: tollsHint, + mapStyleConfig: mapStyleConfig + ) + } + let limit: Range + if annotateAtManeuver { + let deviationOffset = alternativeRoute.deviationOffset() + limit = (deviationOffset + 0.01)..<(deviationOffset + 0.05) + } else { + limit = 0.2..<0.8 + } + if let geometry = alternativeRoute.route.geometryForCallout(clampedTo: limit) { + annotations.append( + ViewAnnotation( + annotatedFeature: .geometry(geometry), + view: etaView + ) + ) + } else { + annotations.append( + ViewAnnotation( + layerId: FeatureIds.RouteAnnotation.alternative(index: idx).layerId, + view: etaView + ) + ) + } + } + } + annotations.forEach { + guard let etaView = $0.view as? ETAView else { return } + $0.setup(with: etaView) + } + self.id = featureId + self.viewAnnotations = annotations + } + + func add(to mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { + for annotation in viewAnnotations { + mapView.viewAnnotations.add(annotation) + } + } + + func remove(from mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { + viewAnnotations.forEach { $0.remove() } + } + + func update(oldValue: any MapFeature, in mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { + oldValue.remove(from: mapView, order: &order) + add(to: mapView, order: &order) + } +} + +extension Route { + fileprivate func geometryForCallout(clampedTo range: Range = 0.2..<0.8) -> Geometry? { + return shape?.trimmed( + from: distance * range.lowerBound, + to: distance * range.upperBound + )?.geometry + } + + fileprivate var containsTolls: Bool { + !(tollIntersections?.isEmpty ?? true) + } +} + +extension ViewAnnotation { + fileprivate func setup(with etaView: ETAView) { + ignoreCameraPadding = true + onAnchorChanged = { config in + etaView.anchor = config.anchor + } + variableAnchors = [ViewAnnotationAnchor.bottomLeft, .bottomRight, .topLeft, .topRight].map { + ViewAnnotationAnchorConfig(anchor: $0) + } + setNeedsUpdateSize() + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift new file mode 100644 index 000000000..2ddd6d67f --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift @@ -0,0 +1,239 @@ +import Foundation +import MapboxMaps +import Turf + +/// Simplifies source/layer/image managements for MapView +/// +/// ## Supported features: +/// +/// ### Layers +/// +/// Can be added/removed but not updated. Custom update logic can be performed using `onUpdate` callback. This +/// is done for performance reasons and to simplify implementation as map layers doesn't support equatable protocol. +/// If you want to update layers, you can consider assigning updated layer a new id. +/// +/// It there is only one source, layers will get it assigned automatically, overwise, layers should has source set +/// manually. +/// +/// ### Sources +/// +/// Sources can also be added/removed, but unlike layers, sources are always updated. +/// +/// +struct GeoJsonMapFeature: MapFeature { + struct Source { + let id: String + let geoJson: GeoJSONObject + } + + typealias LayerId = String + typealias SourceId = String + + let id: String + let sources: [SourceId: Source] + + let customizeSource: @MainActor (_ source: inout GeoJSONSource, _ id: String) -> Void + + let layers: [LayerId: any Layer] + + // MARK: Lifecycle callbacks + + let onBeforeAdd: @MainActor (_ mapView: MapView) -> Void + let onAfterAdd: @MainActor (_ mapView: MapView) -> Void + let onUpdate: @MainActor (_ mapView: MapView) throws -> Void + let onAfterUpdate: @MainActor (_ mapView: MapView) throws -> Void + let onAfterRemove: @MainActor (_ mapView: MapView) -> Void + + init( + id: String, + sources: [Source], + customizeSource: @escaping @MainActor (_: inout GeoJSONSource, _ id: String) -> Void, + layers: [any Layer], + onBeforeAdd: @escaping @MainActor (_: MapView) -> Void = { _ in }, + onAfterAdd: @escaping @MainActor (_: MapView) -> Void = { _ in }, + onUpdate: @escaping @MainActor (_: MapView) throws -> Void = { _ in }, + onAfterUpdate: @escaping @MainActor (_: MapView) throws -> Void = { _ in }, + onAfterRemove: @escaping @MainActor (_: MapView) -> Void = { _ in } + ) { + self.id = id + self.sources = Dictionary(uniqueKeysWithValues: sources.map { ($0.id, $0) }) + self.customizeSource = customizeSource + self.layers = Dictionary(uniqueKeysWithValues: layers.map { ($0.id, $0) }) + self.onBeforeAdd = onBeforeAdd + self.onAfterAdd = onAfterAdd + self.onUpdate = onUpdate + self.onAfterUpdate = onAfterUpdate + self.onAfterRemove = onAfterRemove + } + + // MARK: - MapFeature conformance + + @MainActor + func add(to mapView: MapView, order: inout MapLayersOrder) { + onBeforeAdd(mapView) + + let map: MapboxMap = mapView.mapboxMap + for (_, source) in sources { + addSource(source, to: map) + } + + for (_, var layer) in layers { + addLayer(&layer, to: map, order: &order) + } + + onAfterAdd(mapView) + } + + @MainActor + private func addLayer(_ layer: inout any Layer, to map: MapboxMap, order: inout MapLayersOrder) { + do { + if map.layerExists(withId: layer.id) { + try map.removeLayer(withId: layer.id) + } + order.insert(id: layer.id) + if let slot = order.slot(forId: layer.id), map.allSlotIdentifiers.contains(slot) { + layer.slot = slot + } + try map.addLayer(layer, layerPosition: order.position(forId: layer.id)) + } catch { + Log.error("Failed to add layer '\(layer.id)': \(error)", category: .navigationUI) + } + } + + @MainActor + private func addSource(_ source: Source, to map: MapboxMap) { + do { + if map.sourceExists(withId: source.id) { + map.updateGeoJSONSource( + withId: source.id, + geoJSON: source.geoJson + ) + } else { + var geoJsonSource = GeoJSONSource(id: source.id) + geoJsonSource.data = source.geoJson.sourceData + customizeSource(&geoJsonSource, source.id) + try map.addSource(geoJsonSource) + } + } catch { + Log.error("Failed to add source '\(source.id)': \(error)", category: .navigationUI) + } + } + + @MainActor + func update(oldValue: any MapFeature, in mapView: MapView, order: inout MapLayersOrder) { + guard let oldValue = oldValue as? Self else { + preconditionFailure("Incorrect type passed for oldValue") + } + + for (_, source) in sources { + guard mapView.mapboxMap.sourceExists(withId: source.id) + else { + // In case the map style was changed and the source is missing we're re-adding it back. + oldValue.remove(from: mapView, order: &order) + remove(from: mapView, order: &order) + add(to: mapView, order: &order) + return + } + } + + do { + try onUpdate(mapView) + let map: MapboxMap = mapView.mapboxMap + + let diff = diff(oldValue: oldValue, newValue: self) + for var addedLayer in diff.addedLayers { + addLayer(&addedLayer, to: map, order: &order) + } + for removedLayer in diff.removedLayers { + removeLayer(removedLayer, from: map, order: &order) + } + for addedSource in diff.addedSources { + addSource(addedSource, to: map) + } + for removedSource in diff.removedSources { + removeSource(removedSource.id, from: map) + } + + for (_, source) in sources { + mapView.mapboxMap.updateGeoJSONSource( + withId: source.id, + geoJSON: source.geoJson + ) + } + try onAfterUpdate(mapView) + } catch { + Log.error("Failed to update map feature '\(id)': \(error)", category: .navigationUI) + } + } + + @MainActor + func remove(from mapView: MapView, order: inout MapLayersOrder) { + let map: MapboxMap = mapView.mapboxMap + + for (_, layer) in layers { + removeLayer(layer, from: map, order: &order) + } + + for sourceId in sources.keys { + removeSource(sourceId, from: map) + } + + onAfterRemove(mapView) + } + + @MainActor + private func removeLayer(_ layer: any Layer, from map: MapboxMap, order: inout MapLayersOrder) { + guard map.layerExists(withId: layer.id) else { return } + do { + try map.removeLayer(withId: layer.id) + order.remove(id: layer.id) + } catch { + Log.error("Failed to remove layer '\(layer.id)': \(error)", category: .navigationUI) + } + } + + @MainActor + private func removeSource(_ sourceId: SourceId, from map: MapboxMap) { + if map.sourceExists(withId: sourceId) { + do { + try map.removeSource(withId: sourceId) + } catch { + Log.error("Failed to remove source '\(sourceId)': \(error)", category: .navigationUI) + } + } + } + + // MARK: Diff + + private struct Diff { + let addedLayers: [any Layer] + let removedLayers: [any Layer] + let addedSources: [Source] + let removedSources: [Source] + } + + private func diff(oldValue: Self, newValue: Self) -> Diff { + .init( + addedLayers: newValue.layers.filter { oldValue.layers[$0.key] == nil }.map(\.value), + removedLayers: oldValue.layers.filter { newValue.layers[$0.key] == nil }.map(\.value), + addedSources: newValue.sources.filter { oldValue.sources[$0.key] == nil }.map(\.value), + removedSources: oldValue.sources.filter { newValue.sources[$0.key] == nil }.map(\.value) + ) + } +} + +// MARK: Helpers + +extension GeoJSONObject { + /// Ported from MapboxMaps as the same var is internal in the SDK. + fileprivate var sourceData: GeoJSONSourceData { + switch self { + case .geometry(let geometry): + return .geometry(geometry) + case .feature(let feature): + return .feature(feature) + case .featureCollection(let collection): + return .featureCollection(collection) + } + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift new file mode 100644 index 000000000..36263f283 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift @@ -0,0 +1,16 @@ +// import Foundation +// import MapboxMaps + +// /// Something that can be added/removed/updated in MapboxMaps.MapView. +// /// +// /// Use ``MapFeaturesStore`` to manage a set of features. +// protocol MapFeature { +// var id: String { get } + +// @MainActor +// func add(to mapView: MapView, order: inout MapLayersOrder) +// @MainActor +// func remove(from mapView: MapView, order: inout MapLayersOrder) +// @MainActor +// func update(oldValue: MapFeature, in mapView: MapView, order: inout MapLayersOrder) +// } diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift new file mode 100644 index 000000000..1f5cb49fb --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift @@ -0,0 +1,117 @@ +import Foundation +import MapboxMaps + +/// A store for ``MapFeature``s. +/// +/// It handle style reload by re-adding currently active features (make sure you call `styleLoaded` method). +/// Use `update(using:)` method to provide a new snapshot of features that are managed by this store. The store will +/// handle updates/removes/additions to the map view. +@MainActor +final class MapFeaturesStore { + private struct Features: Sequence { + private var features: [String: any MapFeature] = [:] + + func makeIterator() -> some IteratorProtocol { + features.values.makeIterator() + } + + subscript(_ id: String) -> (any MapFeature)? { + features[id] + } + + mutating func insert(_ feature: any MapFeature) { + features[feature.id] = feature + } + + mutating func remove(_ feature: any MapFeature) -> (any MapFeature)? { + features.removeValue(forKey: feature.id) + } + + mutating func removeAll() -> some Sequence { + let allFeatures = features.values + features = [:] + return allFeatures + } + } + + private let mapView: MapView + private var styleLoadSubscription: MapboxMaps.Cancelable? + private var features: Features = .init() + + private var currentStyleLoaded: Bool = false + private var currentStyleUri: StyleURI? + + private var styleLoaded: Bool { + if currentStyleUri != mapView.mapboxMap.styleURI { + currentStyleLoaded = false + } + return currentStyleLoaded + } + + init(mapView: MapView) { + self.mapView = mapView + self.currentStyleUri = mapView.mapboxMap.styleURI + self.currentStyleLoaded = mapView.mapboxMap.isStyleLoaded + } + + func deactivate(order: inout MapLayersOrder) { + styleLoadSubscription?.cancel() + guard styleLoaded else { return } + features.forEach { $0.remove(from: mapView, order: &order) } + } + + func update(using allFeatures: [any MapFeature]?, order: inout MapLayersOrder) { + guard let allFeatures, !allFeatures.isEmpty else { + removeAll(order: &order); return + } + + let newFeatureIds = Set(allFeatures.map(\.id)) + for existingFeature in features where !newFeatureIds.contains(existingFeature.id) { + remove(existingFeature, order: &order) + } + + for feature in allFeatures { + update(feature, order: &order) + } + } + + private func removeAll(order: inout MapLayersOrder) { + let allFeatures = features.removeAll() + guard styleLoaded else { return } + + for feature in allFeatures { + feature.remove(from: mapView, order: &order) + } + } + + private func update(_ feature: any MapFeature, order: inout MapLayersOrder) { + defer { + features.insert(feature) + } + + guard styleLoaded else { return } + + if let oldFeature = features[feature.id] { + feature.update(oldValue: oldFeature, in: mapView, order: &order) + } else { + feature.add(to: mapView, order: &order) + } + } + + private func remove(_ feature: some MapFeature, order: inout MapLayersOrder) { + guard let removeFeature = features.remove(feature) else { return } + + if styleLoaded { + removeFeature.remove(from: mapView, order: &order) + } + } + + func styleLoaded(order: inout MapLayersOrder) { + currentStyleUri = mapView.mapboxMap.styleURI + currentStyleLoaded = true + + for feature in features { + feature.add(to: mapView, order: &order) + } + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift new file mode 100644 index 000000000..830f23d92 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift @@ -0,0 +1,38 @@ +import MapboxMaps + +extension MapboxMap { + /// Adds image to style if it doesn't exist already and log any errors that occur. + func provisionImage(id: String, _ addImageToMap: (MapboxMap) throws -> Void) { + if !imageExists(withId: id) { + do { + try addImageToMap(self) + } catch { + Log.error("Failed to add image (id: \(id)) to style with error \(error)", category: .navigationUI) + } + } + } + + func setRouteLineOffset( + _ offset: Double, + for routeLineIds: FeatureIds.RouteLine + ) { + guard offset >= 0.0 else { return } + do { + let layerIds: [String] = [ + routeLineIds.main, + routeLineIds.casing, + routeLineIds.restrictedArea, + ] + + for layerId in layerIds where layerExists(withId: layerId) { + try setLayerProperty( + for: layerId, + property: "line-trim-offset", + value: [0.0, Double.minimum(1.0, offset)] + ) + } + } catch { + Log.error("Failed to update route line gradient with error: \(error)", category: .navigationUI) + } + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapLayersOrder.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapLayersOrder.swift new file mode 100644 index 000000000..bdb23e842 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/MapLayersOrder.swift @@ -0,0 +1,260 @@ +import _MapboxNavigationHelpers +import MapboxMaps + +/// Allows to order layers with easy by defining order rules and then query order for any added layer. +struct MapLayersOrder { + @resultBuilder + enum Builder { + static func buildPartialBlock(first rule: Rule) -> [Rule] { + [rule] + } + + static func buildPartialBlock(first slottedRules: SlottedRules) -> [Rule] { + slottedRules.rules + } + + static func buildPartialBlock(accumulated rules: [Rule], next rule: Rule) -> [Rule] { + with(rules) { + $0.append(rule) + } + } + + static func buildPartialBlock(accumulated rules: [Rule], next slottedRules: SlottedRules) -> [Rule] { + rules + slottedRules.rules + } + } + + struct SlottedRules { + let rules: [MapLayersOrder.Rule] + + init(_ slot: Slot?, @MapLayersOrder.Builder rules: () -> [Rule]) { + self.rules = rules().map { rule in + with(rule) { $0.slot = slot } + } + } + } + + struct Rule { + struct MatchPredicate { + let block: (String) -> Bool + + static func hasPrefix(_ prefix: String) -> Self { + .init { + $0.hasPrefix(prefix) + } + } + + static func contains(_ substring: String) -> Self { + .init { + $0.contains(substring) + } + } + + static func exact(_ id: String) -> Self { + .init { + $0 == id + } + } + + static func any(of ids: any Sequence) -> Self { + let set = Set(ids) + return .init { + set.contains($0) + } + } + } + + struct OrderedAscendingComparator { + let block: (_ lhs: String, _ rhs: String) -> Bool + + static func constant(_ value: Bool) -> Self { + .init { _, _ in + value + } + } + + static func order(_ ids: [String]) -> Self { + return .init { lhs, rhs in + guard let lhsIndex = ids.firstIndex(of: lhs), + let rhsIndex = ids.firstIndex(of: rhs) else { return true } + return lhsIndex < rhsIndex + } + } + } + + let matches: (String) -> Bool + let isOrderedAscending: (_ lhs: String, _ rhs: String) -> Bool + var slot: Slot? + + init( + predicate: MatchPredicate, + isOrderedAscending: OrderedAscendingComparator + ) { + self.matches = predicate.block + self.isOrderedAscending = isOrderedAscending.block + } + + static func hasPrefix( + _ prefix: String, + isOrderedAscending: OrderedAscendingComparator = .constant(true) + ) -> Rule { + Rule(predicate: .hasPrefix(prefix), isOrderedAscending: isOrderedAscending) + } + + static func contains( + _ substring: String, + isOrderedAscending: OrderedAscendingComparator = .constant(true) + ) -> Rule { + Rule(predicate: .contains(substring), isOrderedAscending: isOrderedAscending) + } + + static func exact( + _ id: String, + isOrderedAscending: OrderedAscendingComparator = .constant(true) + ) -> Rule { + Rule(predicate: .exact(id), isOrderedAscending: isOrderedAscending) + } + + static func orderedIds(_ ids: [String]) -> Rule { + return Rule( + predicate: .any(of: ids), + isOrderedAscending: .order(ids) + ) + } + + func slotted(_ slot: Slot) -> Self { + with(self) { + $0.slot = slot + } + } + } + + /// Ids that are managed by map style. + private var styleIds: [String] = [] + /// Ids that are managed by SDK. + private var customIds: Set = [] + /// Merged `styleIds` and `customIds` in order defined by rules. + private var orderedIds: [String] = [] + /// A map from id to position in `orderedIds` to speed up `position(forId:)` query. + private var orderedIdsIndices: [String: Int] = [:] + private var idToSlot: [String: Slot] = [:] + /// Ordered list of rules that define order. + private let rules: [Rule] + + /// Used for styles with no slots support. + private let legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? + + init( + @MapLayersOrder.Builder builder: () -> [Rule], + legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? + ) { + self.rules = builder() + self.legacyPosition = legacyPosition + } + + /// Inserts a new id and makes it possible to use it in `position(forId:)` method. + mutating func insert(id: String) { + customIds.insert(id) + + guard let ruleIndex = rules.firstIndex(where: { $0.matches(id) }) else { + orderedIds.append(id) + orderedIdsIndices[id] = orderedIds.count - 1 + return + } + + func binarySearch() -> Int { + var left = 0 + var right = orderedIds.count + + while left < right { + let mid = left + (right - left) / 2 + if let currentRuleIndex = rules.firstIndex(where: { $0.matches(orderedIds[mid]) }) { + if currentRuleIndex > ruleIndex { + right = mid + } else if currentRuleIndex == ruleIndex { + if !rules[ruleIndex].isOrderedAscending(orderedIds[mid], id) { + right = mid + } else { + left = mid + 1 + } + } else { + left = mid + 1 + } + } else { + right = mid + } + } + return left + } + + let insertionIndex = binarySearch() + orderedIds.insert(id, at: insertionIndex) + + // Update the indices of the elements after the insertion point + for index in insertionIndex.. LayerPosition? { + if let legacyPosition { + return legacyPosition(id) + } + + guard let index = orderedIdsIndices[id] else { return nil } + let belowId = index == 0 ? nil : orderedIds[index - 1] + let aboveId = index == orderedIds.count - 1 ? nil : orderedIds[index + 1] + + if let belowId { + return .above(belowId) + } else if let aboveId { + return .below(aboveId) + } else { + return nil + } + } + + func slot(forId id: String) -> Slot? { + idToSlot[id] + } + + private func rule(matching id: String) -> Rule? { + rules.first { rule in + rule.matches(id) + } + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapStyleManager.swift b/ios/Classes/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift similarity index 100% rename from ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapStyleManager.swift rename to ios/Classes/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift new file mode 100644 index 000000000..e4566f14a --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift @@ -0,0 +1,373 @@ +import _MapboxNavigationHelpers +import MapboxDirections +import MapboxMaps +import MapboxNavigationNative +import enum SwiftUI.ColorScheme +import UIKit +import Foundation + +extension NavigationRoutes { + func routeAlertsAnnotationsMapFeatures( + ids: FeatureIds.RouteAlertAnnotation, + distanceTraveled: CLLocationDistance, + customizedLayerProvider: CustomizedLayerProvider, + excludedRouteAlertTypes: RoadAlertType + ) -> [MapFeature] { + let convertedRouteAlerts = mainRoute.nativeRoute.getRouteInfo().alerts.map { + RoadObjectAhead( + roadObject: RoadObject($0.roadObject), + distance: $0.distanceToStart + ) + } + + return convertedRouteAlerts.routeAlertsAnnotationsMapFeatures( + ids: ids, + distanceTraveled: distanceTraveled, + customizedLayerProvider: customizedLayerProvider, + excludedRouteAlertTypes: excludedRouteAlertTypes + ) + } +} + +extension [RoadObjectAhead] { + func routeAlertsAnnotationsMapFeatures( + ids: FeatureIds.RouteAlertAnnotation, + distanceTraveled: CLLocationDistance, + customizedLayerProvider: CustomizedLayerProvider, + excludedRouteAlertTypes: RoadAlertType + ) -> [MapFeature] { + let featureCollection = FeatureCollection(features: roadObjectsFeatures( + for: self, + currentDistance: distanceTraveled, + excludedRouteAlertTypes: excludedRouteAlertTypes + )) + let layers: [any Layer] = [ + with(SymbolLayer(id: ids.layer, source: ids.source)) { + $0.iconImage = .expression(Exp(.get) { RoadObjectInfo.objectImageType }) + $0.minZoom = 10 + + $0.iconSize = .expression( + Exp(.interpolate) { + Exp(.linear) + Exp(.zoom) + Self.interpolationFactors.mapValues { $0 * 0.2 } + } + ) + + $0.iconColor = .expression(Exp(.get) { RoadObjectInfo.objectColor }) + }, + ] + return [ + GeoJsonMapFeature( + id: ids.featureId, + sources: [ + .init( + id: ids.source, + geoJson: .featureCollection(featureCollection) + ), + ], + customizeSource: { _, _ in }, + layers: layers.map { customizedLayerProvider.customizedLayer($0) }, + onBeforeAdd: { mapView in + Self.upsertRouteAlertsSymbolImages( + map: mapView.mapboxMap + ) + }, + onUpdate: { mapView in + Self.upsertRouteAlertsSymbolImages( + map: mapView.mapboxMap + ) + }, + onAfterRemove: { mapView in + do { + try Self.removeRouteAlertSymbolImages( + from: mapView.mapboxMap + ) + } catch { + Log.error( + "Failed to remove route alerts annotation images with error \(error)", + category: .navigationUI + ) + } + } + ), + ] + } + + private static let interpolationFactors = [ + 10.0: 1.0, + 14.5: 3.0, + 17.0: 6.0, + 22.0: 8.0, + ] + + private func roadObjectsFeatures( + for alerts: [RoadObjectAhead], + currentDistance: CLLocationDistance, + excludedRouteAlertTypes: RoadAlertType + ) -> [Feature] { + var features = [Feature]() + for alert in alerts where !alert.isExcluded(excludedRouteAlertTypes: excludedRouteAlertTypes) { + guard alert.distance == nil || alert.distance! >= currentDistance, + let objectInfo = info(for: alert.roadObject.kind) + else { continue } + let object = alert.roadObject + func addImage( + _ coordinate: LocationCoordinate2D, + _ distance: LocationDistance?, + color: UIColor? = nil + ) { + var feature = Feature(geometry: .point(.init(coordinate))) + let identifier: FeatureIdentifier = + .string("road-alert-\(coordinate.latitude)-\(coordinate.longitude)-\(features.count)") + let colorHex = (color ?? objectInfo.color ?? UIColor.gray).hexString + let properties: [String: JSONValue?] = [ + RoadObjectInfo.objectColor: JSONValue(rawValue: colorHex ?? UIColor.gray.hexString!), + RoadObjectInfo.objectImageType: .string(objectInfo.imageType.rawValue), + RoadObjectInfo.objectDistanceFromStart: .number(distance ?? 0.0), + RoadObjectInfo.distanceTraveled: .number(0.0), + ] + feature.properties = properties + feature.identifier = identifier + features.append(feature) + } + switch object.location { + case .routeAlert(shape: .lineString(let shape)): + guard + let startCoordinate = shape.coordinates.first, + let endCoordinate = shape.coordinates.last + else { + break + } + + if alert.distance.map({ $0 > 0 }) ?? true { + addImage(startCoordinate, alert.distance, color: .blue) + } + addImage(endCoordinate, alert.distance.map { $0 + (object.length ?? 0) }, color: .red) + case .routeAlert(shape: .point(let point)): + addImage(point.coordinates, alert.distance, color: nil) + case .openLRPoint(position: _, sideOfRoad: _, orientation: _, coordinate: let coordinates): + addImage(coordinates, alert.distance, color: nil) + case .openLRLine(path: _, shape: let geometry): + guard + let shape = openLRShape(from: geometry), + let startCoordinate = shape.coordinates.first, + let endCoordinate = shape.coordinates.last + else { + break + } + if alert.distance.map({ $0 > 0 }) ?? true { + addImage(startCoordinate, alert.distance, color: .blue) + } + addImage(endCoordinate, alert.distance.map { $0 + (object.length ?? 0) }, color: .red) + case .subgraph(enters: let enters, exits: let exits, shape: _, edges: _): + for enter in enters { + addImage(enter.coordinate, nil, color: .blue) + } + for exit in exits { + addImage(exit.coordinate, nil, color: .red) + } + default: + Log.error( + "Unexpected road object as Route Alert: \(object.identifier):\(object.kind)", + category: .navigationUI + ) + } + } + return features + } + + private func openLRShape(from geometry: Geometry) -> LineString? { + switch geometry { + case .point(let point): + return .init([point.coordinates]) + case .lineString(let lineString): + return lineString + default: + break + } + return nil + } + + private func info(for objectKind: RoadObject.Kind) -> RoadObjectInfo? { + switch objectKind { + case .incident(let incident): + let text = incident?.description + let color = incident?.impact.map(color(for:)) + switch incident?.kind { + case .congestion: + return .init(.congestion, text: text, color: color) + case .construction: + return .init(.construction, text: text, color: color) + case .roadClosure: + return .init(.roadClosure, text: text, color: color) + case .accident: + return .init(.accident, text: text, color: color) + case .disabledVehicle: + return .init(.disabledVehicle, text: text, color: color) + case .laneRestriction: + return .init(.laneRestriction, text: text, color: color) + case .massTransit: + return .init(.massTransit, text: text, color: color) + case .miscellaneous: + return .init(.miscellaneous, text: text, color: color) + case .otherNews: + return .init(.otherNews, text: text, color: color) + case .plannedEvent: + return .init(.plannedEvent, text: text, color: color) + case .roadHazard: + return .init(.roadHazard, text: text, color: color) + case .weather: + return .init(.weather, text: text, color: color) + case .undefined, .none: + return nil + } + default: + // We only show incidents on the map + return nil + } + } + + private func color(for impact: Incident.Impact) -> UIColor { + switch impact { + case .critical: + return .red + case .major: + return .purple + case .minor: + return .orange + case .low: + return .blue + case .unknown: + return .gray + } + } + + public static func resourceBundle() -> Bundle? { + let bundle = Bundle(for: MapboxNavigationProvider.self) + if let resourceBundleURL = bundle.url(forResource: "MapboxNavigationCoreResources", withExtension: "bundle") { + return Bundle(url: resourceBundleURL) + } + return nil + } + + private static func upsertRouteAlertsSymbolImages( + map: MapboxMap + ) { + for (imageName, imageIdentifier) in imageNameToMapIdentifier(ids: RoadObjectFeature.ImageType.allCases) { + if let image = resourceBundle()?.image(named: imageName) { + map.provisionImage(id: imageIdentifier) { _ in + try map.addImage(image, id: imageIdentifier) + } + } else { + assertionFailure("No image for route alert \(imageName) in the bundle.") + } + } + } + + private static func removeRouteAlertSymbolImages( + from map: MapboxMap + ) throws { + for (_, imageIdentifier) in imageNameToMapIdentifier(ids: RoadObjectFeature.ImageType.allCases) { + try map.removeImage(withId: imageIdentifier) + } + } + + private static func imageNameToMapIdentifier( + ids: [RoadObjectFeature.ImageType] + ) -> [String: String] { + return ids.reduce(into: [String: String]()) { partialResult, type in + partialResult[type.imageName] = type.rawValue + } + } + + private struct RoadObjectFeature: Equatable { + enum ImageType: String, CaseIterable { + case accident + case congestion + case construction + case disabledVehicle = "disabled_vehicle" + case laneRestriction = "lane_restriction" + case massTransit = "mass_transit" + case miscellaneous + case otherNews = "other_news" + case plannedEvent = "planned_event" + case roadClosure = "road_closure" + case roadHazard = "road_hazard" + case weather + + var imageName: String { + switch self { + case .accident: + return "ra_accident" + case .congestion: + return "ra_congestion" + case .construction: + return "ra_construction" + case .disabledVehicle: + return "ra_disabled_vehicle" + case .laneRestriction: + return "ra_lane_restriction" + case .massTransit: + return "ra_mass_transit" + case .miscellaneous: + return "ra_miscellaneous" + case .otherNews: + return "ra_other_news" + case .plannedEvent: + return "ra_planned_event" + case .roadClosure: + return "ra_road_closure" + case .roadHazard: + return "ra_road_hazard" + case .weather: + return "ra_weather" + } + } + } + + struct Image: Equatable { + var id: String? + var type: ImageType + var coordinate: LocationCoordinate2D + var color: UIColor? + var text: String? + var isOnMainRoute: Bool + } + + struct Shape: Equatable { + var geometry: Geometry + } + + var id: String + var images: [Image] + var shape: Shape? + } + + private struct RoadObjectInfo { + var imageType: RoadObjectFeature.ImageType + var text: String? + var color: UIColor? + + init(_ imageType: RoadObjectFeature.ImageType, text: String? = nil, color: UIColor? = nil) { + self.imageType = imageType + self.text = text + self.color = color + } + + static let objectColor = "objectColor" + static let objectImageType = "objectImageType" + static let objectDistanceFromStart = "objectDistanceFromStart" + static let distanceTraveled = "distanceTraveled" + } +} + +extension RoadObjectAhead { + fileprivate func isExcluded(excludedRouteAlertTypes: RoadAlertType) -> Bool { + guard let roadAlertType = RoadAlertType(roadObjectKind: roadObject.kind) else { + return false + } + + return excludedRouteAlertTypes.contains(roadAlertType) + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift new file mode 100644 index 000000000..90c7a55bd --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift @@ -0,0 +1,55 @@ +import _MapboxNavigationHelpers +import CoreLocation +import MapboxDirections +import MapboxMaps +import Turf +import UIKit + +/// Describes the possible annotation types on the route line. +public enum RouteAnnotationKind { + /// Shows the route duration. + case routeDurations + /// Shows the relative diff between the main route and the alternative. + /// The annotation is displayed in the approximate middle of the alternative steps. + case relativeDurationsOnAlternative + /// Shows the relative diff between the main route and the alternative. + /// The annotation is displayed next to the first different maneuver of the alternative road. + case relativeDurationsOnAlternativeManuever +} + +extension NavigationRoutes { + func routeDurationMapFeatures( + annotationKinds: Set, + config: MapStyleConfig + ) -> [any MapFeature] { + var showMainRoute = false + var showAlternatives = false + var showAsRelative = false + var annotateManeuver = false + for annotationKind in annotationKinds { + switch annotationKind { + case .routeDurations: + showMainRoute = true + showAlternatives = config.showsAlternatives + case .relativeDurationsOnAlternative: + showAsRelative = true + showAlternatives = config.showsAlternatives + case .relativeDurationsOnAlternativeManuever: + showAsRelative = true + annotateManeuver = true + showAlternatives = config.showsAlternatives + } + } + + return [ + ETAViewsAnnotationFeature( + for: self, + showMainRoute: showMainRoute, + showAlternatives: showAlternatives, + isRelative: showAsRelative, + annotateAtManeuver: annotateManeuver, + mapStyleConfig: config + ), + ] + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift new file mode 100644 index 000000000..66215f6f4 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift @@ -0,0 +1,406 @@ +import _MapboxNavigationHelpers +import MapboxDirections +@_spi(Experimental) import MapboxMaps +import Turf +import UIKit + +struct LineGradientSettings { + let isSoft: Bool + let baseColor: UIColor + let featureColor: (Turf.Feature) -> UIColor +} + +struct RouteLineFeatureProvider { + var customRouteLineLayer: (String, String) -> Layer? + var customRouteCasingLineLayer: (String, String) -> Layer? + var customRouteRestrictedAreasLineLayer: (String, String) -> Layer? +} + +extension Route { + func routeLineMapFeatures( + ids: FeatureIds.RouteLine, + offset: Double, + isSoftGradient: Bool, + isAlternative: Bool, + config: MapStyleConfig, + featureProvider: RouteLineFeatureProvider, + customizedLayerProvider: CustomizedLayerProvider + ) -> [any MapFeature] { + var features: [any MapFeature] = [] + + if let shape { + let congestionFeatures = congestionFeatures( + legIndex: nil, + rangesConfiguration: config.congestionConfiguration.ranges + ) + let gradientStops = routeLineCongestionGradient( + congestionFeatures: congestionFeatures, + isMain: !isAlternative, + isSoft: isSoftGradient, + config: config + ) + let colors = config.congestionConfiguration.colors + let trafficGradient: Value = .expression( + .routeLineGradientExpression( + gradientStops, + lineBaseColor: isAlternative ? colors.alternativeRouteColors.unknown : colors.mainRouteColors + .unknown, + isSoft: isSoftGradient + ) + ) + + var sources: [GeoJsonMapFeature.Source] = [ + .init( + id: ids.source, + geoJson: .init(Feature(geometry: .lineString(shape))) + ), + ] + + let customRouteLineLayer = featureProvider.customRouteLineLayer(ids.main, ids.source) + let customRouteCasingLineLayer = featureProvider.customRouteCasingLineLayer(ids.casing, ids.source) + var layers: [any Layer] = [ + customRouteLineLayer ?? customizedLayerProvider.customizedLayer(defaultRouteLineLayer( + ids: ids, + isAlternative: isAlternative, + trafficGradient: trafficGradient, + config: config + )), + customRouteCasingLineLayer ?? customizedLayerProvider.customizedLayer(defaultRouteCasingLineLayer( + ids: ids, + isAlternative: isAlternative, + config: config + )), + ] + + if let traversedRouteColor = config.traversedRouteColor, !isAlternative, config.routeLineTracksTraversal { + layers.append( + customizedLayerProvider.customizedLayer(defaultTraversedRouteLineLayer( + ids: ids, + traversedRouteColor: traversedRouteColor, + config: config + )) + ) + } + + let restrictedRoadsFeatures: [Feature]? = config.isRestrictedAreaEnabled ? restrictedRoadsFeatures() : nil + let restrictedAreaGradientExpression: Value? = restrictedRoadsFeatures + .map { routeLineRestrictionsGradient($0, config: config) } + .map { + .expression( + MapboxMaps.Expression.routeLineGradientExpression( + $0, + lineBaseColor: config.routeRestrictedAreaColor + ) + ) + } + + if let restrictedRoadsFeatures, let restrictedAreaGradientExpression { + let shape = LineString(restrictedRoadsFeatures.compactMap { + guard case .lineString(let lineString) = $0.geometry else { + return nil + } + return lineString.coordinates + }.reduce([CLLocationCoordinate2D](), +)) + + sources.append( + .init( + id: ids.restrictedAreaSource, + geoJson: .geometry(.lineString(shape)) + ) + ) + let customRouteRestrictedAreasLine = featureProvider.customRouteRestrictedAreasLineLayer( + ids.restrictedArea, + ids.restrictedAreaSource + ) + + layers.append( + customRouteRestrictedAreasLine ?? + customizedLayerProvider.customizedLayer(defaultRouteRestrictedAreasLine( + ids: ids, + gradientExpression: restrictedAreaGradientExpression, + config: config + )) + ) + } + + features.append( + GeoJsonMapFeature( + id: ids.main, + sources: sources, + customizeSource: { source, _ in + source.lineMetrics = true + source.tolerance = 0.375 + }, + layers: layers, + onAfterAdd: { mapView in + mapView.mapboxMap.setRouteLineOffset(offset, for: ids) + }, + onUpdate: { mapView in + mapView.mapboxMap.setRouteLineOffset(offset, for: ids) + }, + onAfterUpdate: { mapView in + let map: MapboxMap = mapView.mapboxMap + try map.updateLayer(withId: ids.main, type: LineLayer.self, update: { layer in + layer.lineGradient = trafficGradient + }) + if let restrictedAreaGradientExpression { + try map.updateLayer(withId: ids.restrictedArea, type: LineLayer.self, update: { layer in + layer.lineGradient = restrictedAreaGradientExpression + }) + } + } + ) + ) + } + + return features + } + + private func defaultRouteLineLayer( + ids: FeatureIds.RouteLine, + isAlternative: Bool, + trafficGradient: Value, + config: MapStyleConfig + ) -> LineLayer { + let colors = config.congestionConfiguration.colors + let routeColors = isAlternative ? colors.alternativeRouteColors : colors.mainRouteColors + return with(LineLayer(id: ids.main, source: ids.source)) { + $0.lineColor = .constant(.init(routeColors.unknown)) + $0.lineWidth = .expression(.routeLineWidthExpression()) + $0.lineJoin = .constant(.round) + $0.lineCap = .constant(.round) + $0.lineGradient = trafficGradient + $0.lineDepthOcclusionFactor = config.occlusionFactor + $0.lineEmissiveStrength = .constant(1) + } + } + + private func defaultRouteCasingLineLayer( + ids: FeatureIds.RouteLine, + isAlternative: Bool, + config: MapStyleConfig + ) -> LineLayer { + let lineColor = isAlternative ? config.routeAlternateCasingColor : config.routeCasingColor + return with(LineLayer(id: ids.casing, source: ids.source)) { + $0.lineColor = .constant(.init(lineColor)) + $0.lineWidth = .expression(.routeCasingLineWidthExpression()) + $0.lineJoin = .constant(.round) + $0.lineCap = .constant(.round) + $0.lineDepthOcclusionFactor = config.occlusionFactor + $0.lineEmissiveStrength = .constant(1) + } + } + + private func defaultTraversedRouteLineLayer( + ids: FeatureIds.RouteLine, + traversedRouteColor: UIColor, + config: MapStyleConfig + ) -> LineLayer { + return with(LineLayer(id: ids.traversedRoute, source: ids.source)) { + $0.lineColor = .constant(.init(traversedRouteColor)) + $0.lineWidth = .expression(.routeLineWidthExpression()) + $0.lineJoin = .constant(.round) + $0.lineCap = .constant(.round) + $0.lineDepthOcclusionFactor = config.occlusionFactor + $0.lineEmissiveStrength = .constant(1) + } + } + + private func defaultRouteRestrictedAreasLine( + ids: FeatureIds.RouteLine, + gradientExpression: Value?, + config: MapStyleConfig + ) -> LineLayer { + return with(LineLayer(id: ids.restrictedArea, source: ids.restrictedAreaSource)) { + $0.lineColor = .constant(.init(config.routeRestrictedAreaColor)) + $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.5)) + $0.lineJoin = .constant(.round) + $0.lineCap = .constant(.round) + $0.lineOpacity = .constant(0.5) + $0.lineDepthOcclusionFactor = config.occlusionFactor + + $0.lineGradient = gradientExpression + $0.lineDasharray = .constant([0.5, 2.0]) + } + } + + func routeLineCongestionGradient( + congestionFeatures: [Turf.Feature]? = nil, + isMain: Bool = true, + isSoft: Bool, + config: MapStyleConfig + ) -> [Double: UIColor] { + // If `congestionFeatures` is set to nil - check if overridden route line casing is used. + let colors = config.congestionConfiguration.colors + let baseColor: UIColor = if let _ = congestionFeatures { + isMain ? colors.mainRouteColors.unknown : colors.alternativeRouteColors.unknown + } else { + config.routeCasingColor + } + let configuration = config.congestionConfiguration.colors + + let lineSettings = LineGradientSettings( + isSoft: isSoft, + baseColor: baseColor, + featureColor: { + guard config.showsTrafficOnRouteLine else { + return baseColor + } + if case .boolean(let isCurrentLeg) = $0.properties?[CurrentLegAttribute], isCurrentLeg { + let colors = isMain ? configuration.mainRouteColors : configuration.alternativeRouteColors + if case .string(let congestionLevel) = $0.properties?[CongestionAttribute] { + return congestionColor(for: congestionLevel, with: colors) + } else { + return congestionColor(for: nil, with: colors) + } + } + + return config.routeCasingColor + } + ) + + return routeLineFeaturesGradient(congestionFeatures, lineSettings: lineSettings) + } + + /// Given a congestion level, return its associated color. + func congestionColor(for congestionLevel: String?, with colors: CongestionColorsConfiguration.Colors) -> UIColor { + switch congestionLevel { + case "low": + return colors.low + case "moderate": + return colors.moderate + case "heavy": + return colors.heavy + case "severe": + return colors.severe + default: + return colors.unknown + } + } + + func routeLineFeaturesGradient( + _ routeLineFeatures: [Turf.Feature]? = nil, + lineSettings: LineGradientSettings + ) -> [Double: UIColor] { + var gradientStops = [Double: UIColor]() + var distanceTraveled = 0.0 + + if let routeLineFeatures { + let routeDistance = routeLineFeatures.compactMap { feature -> LocationDistance? in + if case .lineString(let lineString) = feature.geometry { + return lineString.distance() + } else { + return nil + } + }.reduce(0, +) + // lastRecordSegment records the last segmentEndPercentTraveled and associated congestion color added to the + // gradientStops. + var lastRecordSegment: (Double, UIColor) = (0.0, .clear) + + for (index, feature) in routeLineFeatures.enumerated() { + let associatedFeatureColor = lineSettings.featureColor(feature) + + guard case .lineString(let lineString) = feature.geometry, + let distance = lineString.distance() + else { + if gradientStops.isEmpty { + gradientStops[0.0] = lineSettings.baseColor + } + return gradientStops + } + let minimumPercentGap = 2e-16 + let stopGap = (routeDistance > 0.0) ? max( + min(GradientCongestionFadingDistance, distance * 0.1) / routeDistance, + minimumPercentGap + ) : minimumPercentGap + + if index == routeLineFeatures.startIndex { + distanceTraveled = distanceTraveled + distance + gradientStops[0.0] = associatedFeatureColor + + if index + 1 < routeLineFeatures.count { + let segmentEndPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 + var currentGradientStop = lineSettings + .isSoft ? segmentEndPercentTraveled - stopGap : + Double(CGFloat(segmentEndPercentTraveled).nextDown) + currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) + gradientStops[currentGradientStop] = associatedFeatureColor + lastRecordSegment = (currentGradientStop, associatedFeatureColor) + } + + continue + } + + if index == routeLineFeatures.endIndex - 1 { + if associatedFeatureColor == lastRecordSegment.1 { + gradientStops[lastRecordSegment.0] = nil + } else { + let segmentStartPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 + var currentGradientStop = lineSettings + .isSoft ? segmentStartPercentTraveled + stopGap : + Double(CGFloat(segmentStartPercentTraveled).nextUp) + currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) + gradientStops[currentGradientStop] = associatedFeatureColor + } + + continue + } + + if associatedFeatureColor == lastRecordSegment.1 { + gradientStops[lastRecordSegment.0] = nil + } else { + let segmentStartPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 + var currentGradientStop = lineSettings + .isSoft ? segmentStartPercentTraveled + stopGap : + Double(CGFloat(segmentStartPercentTraveled).nextUp) + currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) + gradientStops[currentGradientStop] = associatedFeatureColor + } + + distanceTraveled = distanceTraveled + distance + let segmentEndPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 + var currentGradientStop = lineSettings + .isSoft ? segmentEndPercentTraveled - stopGap : Double(CGFloat(segmentEndPercentTraveled).nextDown) + currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) + gradientStops[currentGradientStop] = associatedFeatureColor + lastRecordSegment = (currentGradientStop, associatedFeatureColor) + } + + if gradientStops.isEmpty { + gradientStops[0.0] = lineSettings.baseColor + } + + } else { + gradientStops[0.0] = lineSettings.baseColor + } + + return gradientStops + } + + func routeLineRestrictionsGradient( + _ restrictionFeatures: [Turf.Feature], + config: MapStyleConfig + ) -> [Double: UIColor] { + // If there's no restricted feature, hide the restricted route line layer. + guard restrictionFeatures.count > 0 else { + let gradientStops: [Double: UIColor] = [0.0: .clear] + return gradientStops + } + + let lineSettings = LineGradientSettings( + isSoft: false, + baseColor: config.routeRestrictedAreaColor, + featureColor: { + if case .boolean(let isRestricted) = $0.properties?[RestrictedRoadClassAttribute], + isRestricted + { + return config.routeRestrictedAreaColor + } + + return .clear // forcing hiding non-restricted areas + } + ) + + return routeLineFeaturesGradient(restrictionFeatures, lineSettings: lineSettings) + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift new file mode 100644 index 000000000..cda245e09 --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift @@ -0,0 +1,67 @@ +import _MapboxNavigationHelpers +import MapboxDirections +import MapboxMaps +import Turf +import MapboxNavigationCore + +extension Route { + func voiceInstructionMapFeatures( + ids: FeatureIds.VoiceInstruction, + customizedLayerProvider: CustomizedLayerProvider + ) -> [any MapFeature] { + var featureCollection = FeatureCollection(features: []) + + for (legIndex, leg) in legs.enumerated() { + for (stepIndex, step) in leg.steps.enumerated() { + guard let instructions = step.instructionsSpokenAlongStep else { continue } + for instruction in instructions { + guard let shape = legs[legIndex].steps[stepIndex].shape, + let coordinateFromStart = LineString(shape.coordinates.reversed()) + .coordinateFromStart(distance: instruction.distanceAlongStep) else { continue } + + var feature = Feature(geometry: .point(Point(coordinateFromStart))) + feature.properties = [ + "instruction": .string(instruction.text), + ] + featureCollection.features.append(feature) + } + } + } + + let layers: [any Layer] = [ + with(SymbolLayer(id: ids.layer, source: ids.source)) { + let instruction = Exp(.toString) { + Exp(.get) { + "instruction" + } + } + + $0.textField = .expression(instruction) + $0.textSize = .constant(14) + $0.textHaloWidth = .constant(1) + $0.textHaloColor = .constant(.init(.white)) + $0.textOpacity = .constant(0.75) + $0.textAnchor = .constant(.bottom) + $0.textJustify = .constant(.left) + }, + with(CircleLayer(id: ids.circleLayer, source: ids.source)) { + $0.circleRadius = .constant(5) + $0.circleOpacity = .constant(0.75) + $0.circleColor = .constant(.init(.white)) + }, + ] + return [ + GeoJsonMapFeature( + id: ids.source, + sources: [ + .init( + id: ids.source, + geoJson: .featureCollection(featureCollection) + ), + ], + customizeSource: { _, _ in }, + layers: layers.map { customizedLayerProvider.customizedLayer($0) } + ), + ] + } +} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift b/ios/Classes/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift new file mode 100644 index 000000000..673da318a --- /dev/null +++ b/ios/Classes/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift @@ -0,0 +1,157 @@ +import _MapboxNavigationHelpers +import MapboxDirections +import MapboxMaps +import Turf +import UIKit +import MapboxNavigationCore + +struct WaypointFeatureProvider { + var customFeatures: ([Waypoint], Int) -> FeatureCollection? + var customCirleLayer: (String, String) -> CircleLayer? + var customSymbolLayer: (String, String) -> SymbolLayer? +} + +@MainActor +extension Route { + /// Generates a map feature that visually represents waypoints along a route line. + /// The waypoints include the start, destination, and any intermediate waypoints. + /// - Important: Only intermediate waypoints are marked with pins. The starting point and destination are excluded + /// from this. + func waypointsMapFeature( + mapView: MapView, + legIndex: Int, + config: MapStyleConfig, + featureProvider: WaypointFeatureProvider, + customizedLayerProvider: CustomizedLayerProvider + ) -> MapFeature? { + guard let startWaypoint = legs.first?.source else { return nil } + guard let destinationWaypoint = legs.last?.destination else { return nil } + + let intermediateWaypoints = config.showsIntermediateWaypoints + ? legs.dropLast().compactMap(\.destination) + : [] + let waypoints = [startWaypoint] + intermediateWaypoints + [destinationWaypoint] + + registerIntermediateWaypointImage(in: mapView) + + let customFeatures = featureProvider.customFeatures(waypoints, legIndex) + + return waypointsMapFeature( + with: customFeatures ?? waypointsFeatures(legIndex: legIndex, waypoints: waypoints), + config: config, + featureProvider: featureProvider, + customizedLayerProvider: customizedLayerProvider + ) + } + + private func waypointsFeatures(legIndex: Int, waypoints: [Waypoint]) -> FeatureCollection { + FeatureCollection( + features: waypoints.enumerated().map { waypointIndex, waypoint in + var feature = Feature(geometry: .point(Point(waypoint.coordinate))) + var properties: [String: JSONValue] = [:] + properties["waypointCompleted"] = .boolean(waypointIndex <= legIndex) + properties["waipointIconImage"] = waypointIndex > 0 && waypointIndex < waypoints.count - 1 + ? .string(NavigationMapView.ImageIdentifier.midpointMarkerImage) + : nil + feature.properties = properties + + return feature + } + ) + } + + private func registerIntermediateWaypointImage(in mapView: MapView) { + let intermediateWaypointImageId = NavigationMapView.ImageIdentifier.midpointMarkerImage + mapView.mapboxMap.provisionImage(id: intermediateWaypointImageId) { + try $0.addImage( + UIImage.midpointMarkerImage, + id: intermediateWaypointImageId, + stretchX: [], + stretchY: [] + ) + } + } + + private func waypointsMapFeature( + with features: FeatureCollection, + config: MapStyleConfig, + featureProvider: WaypointFeatureProvider, + customizedLayerProvider: CustomizedLayerProvider + ) -> MapFeature { + let circleLayer = featureProvider.customCirleLayer( + FeatureIds.RouteWaypoints.default.innerCircle, + FeatureIds.RouteWaypoints.default.source + ) ?? customizedLayerProvider.customizedLayer(defaultCircleLayer(config: config)) + + let symbolLayer = featureProvider.customSymbolLayer( + FeatureIds.RouteWaypoints.default.markerIcon, + FeatureIds.RouteWaypoints.default.source + ) ?? customizedLayerProvider.customizedLayer(defaultSymbolLayer) + + return GeoJsonMapFeature( + id: FeatureIds.RouteWaypoints.default.featureId, + sources: [ + .init( + id: FeatureIds.RouteWaypoints.default.source, + geoJson: .featureCollection(features) + ), + ], + customizeSource: { _, _ in }, + layers: [circleLayer, symbolLayer], + onBeforeAdd: { _ in }, + onAfterRemove: { _ in } + ) + } + + private func defaultCircleLayer(config: MapStyleConfig) -> CircleLayer { + with( + CircleLayer( + id: FeatureIds.RouteWaypoints.default.innerCircle, + source: FeatureIds.RouteWaypoints.default.source + ) + ) { + let opacity = Exp(.switchCase) { + Exp(.any) { + Exp(.get) { + "waypointCompleted" + } + } + 0 + 1 + } + + $0.circleColor = .constant(.init(config.waypointColor)) + $0.circleOpacity = .expression(opacity) + $0.circleEmissiveStrength = .constant(1) + $0.circleRadius = .expression(.routeCasingLineWidthExpression(0.5)) + $0.circleStrokeColor = .constant(.init(config.waypointStrokeColor)) + $0.circleStrokeWidth = .expression(.routeCasingLineWidthExpression(0.14)) + $0.circleStrokeOpacity = .expression(opacity) + $0.circlePitchAlignment = .constant(.map) + } + } + + private var defaultSymbolLayer: SymbolLayer { + with( + SymbolLayer( + id: FeatureIds.RouteWaypoints.default.markerIcon, + source: FeatureIds.RouteWaypoints.default.source + ) + ) { + let opacity = Exp(.switchCase) { + Exp(.any) { + Exp(.get) { + "waypointCompleted" + } + } + 0 + 1 + } + $0.iconOpacity = .expression(opacity) + $0.iconImage = .expression(Exp(.get) { "waipointIconImage" }) + $0.iconAnchor = .constant(.bottom) + $0.iconOffset = .constant([0, 15]) + $0.iconAllowOverlap = .constant(true) + } + } +} diff --git a/ios/Classes/NavigationController+Gestures.swift b/ios/Classes/NavigationController+Gestures.swift index 8897d4d49..5c47c5d6e 100644 --- a/ios/Classes/NavigationController+Gestures.swift +++ b/ios/Classes/NavigationController+Gestures.swift @@ -4,6 +4,7 @@ import MapboxDirections import MapboxMaps import Turf import UIKit +import MapboxNavigationCore extension NavigationController { func setupGestureRecognizers() { @@ -12,7 +13,7 @@ extension NavigationController { target: self, action: #selector(handleLongPress(_:)) ) - addGestureRecognizer(longPressGestureRecognizer) + //addGestureRecognizer(longPressGestureRecognizer) // Gesture recognizer, which is used to detect taps on route line, waypoint or POI mapViewTapGestureRecognizer = UITapGestureRecognizer( @@ -29,10 +30,10 @@ extension NavigationController { @objc private func handleLongPress(_ gesture: UIGestureRecognizer) { guard gesture.state == .began else { return } - let gestureLocation = gesture.location(in: self) + let gestureLocation = gesture.location(in: self.mapView) Task { @MainActor in let point = await mapPoint(at: gestureLocation) - delegate?.navigationMapView(self, userDidLongTap: point) + //delegate?.navigationMapView(self, userDidLongTap: point) } } @@ -81,20 +82,20 @@ extension NavigationController { if let allRoutes = routes?.allRoutes() { let waypointTest = legSeparatingWaypoints(on: allRoutes, closeTo: tapPoint) if let selected = waypointTest?.first { - delegate?.navigationMapView(self, didSelect: selected) + //delegate?.navigationMapView(self, didSelect: selected) return } } if let alternativeRoute = continuousAlternativeRoutes(closeTo: tapPoint)?.first { - delegate?.navigationMapView(self, didSelect: alternativeRoute) + //delegate?.navigationMapView(self, didSelect: alternativeRoute) return } let point = await mapPoint(at: tapPoint) if point.name != nil { - delegate?.navigationMapView(self, userDidTap: point) + //delegate?.navigationMapView(self, userDidTap: point) } } } @@ -127,20 +128,22 @@ extension NavigationController { } private func mapPoint(at point: CGPoint) async -> MapPoint { - let options = RenderedQueryOptions(layerIds: mapStyleManager.poiLayerIds, filter: nil) + let options = MapboxMaps.RenderedQueryOptions(layerIds: mapStyleManager.poiLayerIds, filter: nil) let rectSize = poiClickableAreaSize let rect = CGRect(x: point.x - rectSize / 2, y: point.y - rectSize / 2, width: rectSize, height: rectSize) - let features = try? await mapView.mapboxMap.queryRenderedFeatures(with: rect, options: options) - if let feature = features?.first?.queriedFeature.feature, - case .string(let poiName) = feature[property: .poiName, languageCode: nil], - case .point(let point) = feature.geometry - { - return MapPoint(name: poiName, coordinate: point.coordinates) - } else { - let coordinate = mapView.mapboxMap.coordinate(for: point) - return MapPoint(name: nil, coordinate: coordinate) - } +// let features = try? await mapView.mapboxMap.queryRenderedFeatures(with: rect, options: options) +// if let feature = features?.first?.queriedFeature.feature, +// case .string(let poiName) = feature[property: .poiName, languageCode: nil], +// case .point(let point) = feature.geometry +// { +// return MapPoint(name: poiName, coordinate: point.coordinates) +// } else { +// let coordinate = mapView.mapboxMap.coordinate(for: point) +// return MapPoint(name: nil, coordinate: coordinate) +// } + let coordinate = mapView.mapboxMap.coordinate(for: point) + return MapPoint(name: nil, coordinate: coordinate) } } @@ -154,7 +157,7 @@ extension NavigationController: GestureManagerDelegate { guard gestureType != .singleTap else { return } MainActor.assumingIsolated { - delegate?.navigationMapViewUserDidStartInteraction(self) + //delegate?.navigationMapViewUserDidStartInteraction(self) } } @@ -166,7 +169,7 @@ extension NavigationController: GestureManagerDelegate { guard gestureType != .singleTap else { return } MainActor.assumingIsolated { - delegate?.navigationMapViewUserDidEndInteraction(self) + //delegate?.navigationMapViewUserDidEndInteraction(self) } } diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index 4c013e7f8..ac306a32c 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -34,12 +34,19 @@ final class NavigationController: NSObject, NavigationInterface { var navigationCamera: MapboxNavigationCore.NavigationCamera let mapStyleManager: NavigationMapStyleManager + /// The object that acts as the navigation delegate of the map view. + public weak var delegate: NavigationMapViewDelegate? + // Vanishing route line properties var routePoints: RoutePoints? var routeLineGranularDistances: RouteLineGranularDistances? var routeRemainingDistancesIndex: Int? private var lifetimeSubscriptions: Set = [] + + /// The gesture recognizer, that is used to detect taps on waypoints and routes that are currently + /// present on the map. Enabled by default. + public internal(set) var mapViewTapGestureRecognizer: UITapGestureRecognizer! init(withMapView mapView: MapView, navigationProvider: MapboxNavigationProvider) { @@ -368,7 +375,7 @@ final class NavigationController: NSObject, NavigationInterface { let routes = [routes.mainRoute.route] + routes.alternativeRoutes.map(\.route) coordinates = MultiLineString(routes.compactMap(\.shape?.coordinates)).coordinates.flatMap { $0 } } - let initialCameraOptions = CameraOptions( + let initialCameraOptions = MapboxMaps.CameraOptions( padding: navigationCamera.viewportPadding, bearing: 0, pitch: 0 @@ -510,14 +517,14 @@ final class NavigationController: NSObject, NavigationInterface { func cancelPreview() { waypoints = [] - currentPreviewRoutes = nil + routes = nil update(navigationCameraState: .following) } func startActiveNavigation() { - guard let previewRoutes = currentPreviewRoutes else { return } + guard let previewRoutes = routes else { return } core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) - currentPreviewRoutes = nil + routes = nil waypoints = [] update(navigationCameraState: .following) } @@ -539,7 +546,7 @@ final class NavigationController: NSObject, NavigationInterface { profileIdentifier: profileIdentifier ) let previewRoutes = try await provider.calculateRoutes(options: mapMatchingOptions).value - currentPreviewRoutes = previewRoutes + routes = previewRoutes self.onNavigationListener?.onNavigationRouteReady() { _ in } } else { let routeOptions = NavigationRouteOptions( @@ -547,7 +554,7 @@ final class NavigationController: NSObject, NavigationInterface { profileIdentifier: profileIdentifier ) let previewRoutes = try await provider.calculateRoutes(options: routeOptions).value - currentPreviewRoutes = previewRoutes + routes = previewRoutes self.onNavigationListener?.onNavigationRouteReady() { _ in } } update(navigationCameraState: .idle) @@ -581,10 +588,10 @@ final class NavigationController: NSObject, NavigationInterface { } func startTripSession(withForegroundService: Bool, completion: @escaping (Result) -> Void) { - guard let previewRoutes = currentPreviewRoutes else { return } + guard let previewRoutes = routes else { return } core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) update(navigationCameraState: .following) - currentPreviewRoutes = nil + routes = nil waypoints = [] completion(.success(Void())) } From 743b08f157077fea91610c0403f4dd923ead4b0a Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sun, 9 Feb 2025 08:38:59 +0100 Subject: [PATCH 21/33] try to use navigation view --- example/ios/Podfile.lock | 8 +- ios/Classes/MapboxMapController.swift | 27 +- .../Map/Other/NavigationMapIdentifiers.swift | 37 -- .../Map/Other/UIColor++.swift | 39 -- .../Map/Other/UIFont.swift | 7 - .../Style/AlternativeRoute+Deviation.swift | 29 - .../Map/Style/FeatureIds.swift | 169 ------ .../IntersectionAnnotationsMapFeatures.swift | 142 ----- .../Map/Style/ManeuverArrowMapFeatures.swift | 142 ----- .../Map/Style/MapFeatures/ETAView.swift | 285 ---------- .../ETAViewsAnnotationFeature.swift | 139 ----- .../Style/MapFeatures/GeoJsonMapFeature.swift | 239 -------- .../Map/Style/MapFeatures/MapFeature.swift | 16 - .../Style/MapFeatures/MapFeaturesStore.swift | 117 ---- .../Map/Style/MapFeatures/Style++.swift | 38 -- .../Map/Style/MapLayersOrder.swift | 260 --------- .../Map/Style/NavigationMapStyleManager.swift | 538 ------------------ .../RouteAlertsAnnotationsMapFeatures.swift | 373 ------------ .../Style/RouteAnnotationMapFeatures.swift | 55 -- .../Map/Style/RouteLineMapFeatures.swift | 406 ------------- .../Style/VoiceInstructionsMapFeatures.swift | 67 --- .../Map/Style/WaypointsMapFeature.swift | 157 ----- ...ionController+ContinuousAlternatives.swift | 65 --- .../NavigationController+Gestures.swift | 209 ------- ...igationController+VanishingRouteLine.swift | 213 ------- ios/Classes/NavigationController.swift | 530 +---------------- ios/mapbox_maps_flutter.podspec | 2 +- 27 files changed, 54 insertions(+), 4255 deletions(-) delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Other/UIColor++.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Other/UIFont.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/FeatureIds.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/MapLayersOrder.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift delete mode 100644 ios/Classes/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift delete mode 100644 ios/Classes/NavigationController+ContinuousAlternatives.swift delete mode 100644 ios/Classes/NavigationController+Gestures.swift delete mode 100644 ios/Classes/NavigationController+VanishingRouteLine.swift diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9d4743cc8..46c27ff5a 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -5,7 +5,7 @@ PODS: - mapbox_maps_flutter (2.4.0): - Flutter - MapboxMaps (= 11.8.0) - - MapboxNavigationCoreUnofficial (= 3.5.0) + - MapboxNavigationCoreUnofficial (= 3.5.1) - Turf (= 3.0.0) - MapboxCommon (24.8.0) - MapboxCoreMaps (11.8.0): @@ -16,7 +16,7 @@ PODS: - MapboxCommon (= 24.8.0) - MapboxCoreMaps (= 11.8.0) - Turf (= 3.0.0) - - MapboxNavigationCoreUnofficial (3.5.0): + - MapboxNavigationCoreUnofficial (3.5.1): - MapboxDirectionsUnofficial (= 3.5.0) - MapboxMaps (= 11.8.0) - MapboxNavigationHelpersUnofficial (= 3.5.0) @@ -64,12 +64,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - mapbox_maps_flutter: c32deef4a666ff7a7581eba0becf24a5c9381ed9 + mapbox_maps_flutter: ec1446f389200627f54e67adc8a854036724ddad MapboxCommon: 95fe03b74d0d0ca39dc646ca14862deb06875151 MapboxCoreMaps: f2a82182c5f6c6262220b81547c6df708012932b MapboxDirectionsUnofficial: 4244d39727c60672e45800784e121782d55a60ad MapboxMaps: dbe1869006c5918d62efc6b475fb884947ea2ecd - MapboxNavigationCoreUnofficial: ddfd6bd636793d4c4aa6617b19bffaab1354076e + MapboxNavigationCoreUnofficial: 76a196139b4ad56ab47006404130401eacd51a44 MapboxNavigationHelpersUnofficial: 325ef24b1487c336572dad217e35a41be8199eae MapboxNavigationNative: 3a300f654f9673c6e4cc5f6743997cc3a4c5ceae path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index 8be8c10e1..033dd21f6 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -23,8 +23,10 @@ final class MapboxMapController: NSObject, FlutterPlatformView { locationSource: .live )) + private var navigationMapView: NavigationMapView! + func view() -> UIView { - return mapView + return navigationMapView } init( @@ -39,7 +41,26 @@ final class MapboxMapController: NSObject, FlutterPlatformView { _ = SettingsServiceFactory.getInstanceFor(.nonPersistent) .set(key: "com.mapbox.common.telemetry.internal.custom_user_agent_fragment", value: "FlutterPlugin/\(pluginVersion)") - mapView = MapView(frame: frame, mapInitOptions: mapInitOptions) + 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.puckType = .puck2D(.navigationDefault) + //navigationMapView.delegate = self + navigationMapView.translatesAutoresizingMaskIntoConstraints = false + + self.navigationMapView = navigationMapView + + mapView = self.navigationMapView.mapView mapboxMap = mapView.mapboxMap channel = FlutterMethodChannel( @@ -91,7 +112,7 @@ final class MapboxMapController: NSObject, FlutterPlatformView { annotationController = AnnotationController(withMapView: mapView) annotationController!.setup(binaryMessenger: binaryMessenger) - navigationController = NavigationController(withMapView: mapView, navigationProvider: MapboxMapController.navigationProvider ) + navigationController = NavigationController(withMapView: navigationMapView, navigationProvider: mapboxNavigation) NavigationInterfaceSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: navigationController, messageChannelSuffix: binaryMessenger.suffix) super.init() diff --git a/ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift b/ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift deleted file mode 100644 index 17f7a2b57..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Other/NavigationMapIdentifiers.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation - -extension NavigationController { - static let identifier = "com.mapbox.navigation.core" - - @MainActor - enum LayerIdentifier { - static let puck2DLayer: String = "puck" - static let puck3DLayer: String = "puck-model-layer" - static let poiLabelLayer: String = "poi-label" - static let transitLabelLayer: String = "transit-label" - static let airportLabelLayer: String = "airport-label" - - static var clickablePoiLabels: [String] { - [ - LayerIdentifier.poiLabelLayer, - LayerIdentifier.transitLabelLayer, - LayerIdentifier.airportLabelLayer, - ] - } - } - - enum ImageIdentifier { - static let markerImage = "default_marker" - static let midpointMarkerImage = "midpoint_marker" - static let trafficSignal = "traffic_signal" - static let railroadCrossing = "railroad_crossing" - static let yieldSign = "yield_sign" - static let stopSign = "stop_sign" - static let searchAnnotationImage = "search_annotation" - static let selectedSearchAnnotationImage = "search_annotation_selected" - } - - enum ModelKeyIdentifier { - static let modelSouce = "puck-model" - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Other/UIColor++.swift b/ios/Classes/MapboxNavigationCore/Map/Other/UIColor++.swift deleted file mode 100644 index b6d60e505..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Other/UIColor++.swift +++ /dev/null @@ -1,39 +0,0 @@ -import MapboxMaps -import UIKit - -extension UIColor { - public class var defaultTintColor: UIColor { #colorLiteral(red: 0.1843137255, green: 0.4784313725, blue: 0.7764705882, alpha: 1) } - - public class var defaultRouteCasing: UIColor { .defaultTintColor } - public class var defaultRouteLayer: UIColor { #colorLiteral(red: 0.337254902, green: 0.6588235294, blue: 0.9843137255, alpha: 1) } - public class var defaultAlternateLine: UIColor { #colorLiteral(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) } - public class var defaultAlternateLineCasing: UIColor { #colorLiteral(red: 0.5019607843, green: 0.4980392157, blue: 0.5019607843, alpha: 1) } - public class var defaultManeuverArrowStroke: UIColor { .defaultRouteLayer } - public class var defaultManeuverArrow: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } - - public class var trafficUnknown: UIColor { defaultRouteLayer } - public class var trafficLow: UIColor { defaultRouteLayer } - public class var trafficModerate: UIColor { #colorLiteral(red: 1, green: 0.5843137255, blue: 0, alpha: 1) } - public class var trafficHeavy: UIColor { #colorLiteral(red: 1, green: 0.3019607843, blue: 0.3019607843, alpha: 1) } - public class var trafficSevere: UIColor { #colorLiteral(red: 0.5607843137, green: 0.1411764706, blue: 0.2784313725, alpha: 1) } - - public class var alternativeTrafficUnknown: UIColor { defaultAlternateLine } - public class var alternativeTrafficLow: UIColor { defaultAlternateLine } - public class var alternativeTrafficModerate: UIColor { #colorLiteral(red: 0.75, green: 0.63, blue: 0.53, alpha: 1.0) } - public class var alternativeTrafficHeavy: UIColor { #colorLiteral(red: 0.71, green: 0.51, blue: 0.51, alpha: 1.0) } - public class var alternativeTrafficSevere: UIColor { #colorLiteral(red: 0.71, green: 0.51, blue: 0.51, alpha: 1.0) } - - public class var defaultRouteRestrictedAreaColor: UIColor { #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) } - - public class var defaultRouteAnnotationColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } - public class var defaultSelectedRouteAnnotationColor: UIColor { #colorLiteral(red: 0.1882352941, green: 0.4470588235, blue: 0.9607843137, alpha: 1) } - - public class var defaultRouteAnnotationTextColor: UIColor { #colorLiteral(red: 0.01960784314, green: 0.02745098039, blue: 0.03921568627, alpha: 1) } - public class var defaultSelectedRouteAnnotationTextColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } - - public class var defaultRouteAnnotationMoreTimeTextColor: UIColor { #colorLiteral(red: 0.9215686275, green: 0.1450980392, blue: 0.1647058824, alpha: 1) } - public class var defaultRouteAnnotationLessTimeTextColor: UIColor { #colorLiteral(red: 0.03529411765, green: 0.6666666667, blue: 0.4549019608, alpha: 1) } - - public class var defaultWaypointColor: UIColor { #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } - public class var defaultWaypointStrokeColor: UIColor { #colorLiteral(red: 0.137254902, green: 0.1490196078, blue: 0.1764705882, alpha: 1) } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Other/UIFont.swift b/ios/Classes/MapboxNavigationCore/Map/Other/UIFont.swift deleted file mode 100644 index 543c67423..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Other/UIFont.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIKit - -extension UIFont { - public class var defaultRouteAnnotationTextFont: UIFont { - .systemFont(ofSize: 18) - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift b/ios/Classes/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift deleted file mode 100644 index 172b9b1b0..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/AlternativeRoute+Deviation.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation - -extension AlternativeRoute { - /// Returns offset of the alternative route where it deviates from the main route. - func deviationOffset() -> Double { - guard let coordinates = route.shape?.coordinates, - !coordinates.isEmpty - else { - return 0 - } - - let splitGeometryIndex = alternativeRouteIntersectionIndices.routeGeometryIndex - - var totalDistance = 0.0 - var pointDistance: Double? = nil - for index in stride(from: coordinates.count - 1, to: 0, by: -1) { - let currCoordinate = coordinates[index] - let prevCoordinate = coordinates[index - 1] - totalDistance += currCoordinate.projectedDistance(to: prevCoordinate) - - if index == splitGeometryIndex + 1 { - pointDistance = totalDistance - } - } - guard let pointDistance, totalDistance != 0 else { return 0 } - - return (totalDistance - pointDistance) / totalDistance - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/FeatureIds.swift b/ios/Classes/MapboxNavigationCore/Map/Style/FeatureIds.swift deleted file mode 100644 index 994ef0f96..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/FeatureIds.swift +++ /dev/null @@ -1,169 +0,0 @@ - -enum FeatureIds { - private static let globalPrefix: String = "com.mapbox.navigation" - - struct RouteLine: Hashable, Sendable { - private static let prefix: String = "\(globalPrefix).route_line" - - static var main: Self { - .init(routeId: "\(prefix).main") - } - - static func alternative(idx: Int) -> Self { - .init(routeId: "\(prefix).alternative_\(idx)") - } - - let source: String - let main: String - let casing: String - - let restrictedArea: String - let restrictedAreaSource: String - let traversedRoute: String - - init(routeId: String) { - self.source = routeId - self.main = routeId - self.casing = "\(routeId).casing" - self.restrictedArea = "\(routeId).restricted_area" - self.restrictedAreaSource = "\(routeId).restricted_area" - self.traversedRoute = "\(routeId).traversed_route" - } - } - - struct ManeuverArrow { - private static let prefix: String = "\(globalPrefix).arrow" - - let id: String - let symbolId: String - let arrow: String - let arrowStroke: String - let arrowSymbol: String - let arrowSymbolCasing: String - let arrowSource: String - let arrowSymbolSource: String - let triangleTipImage: String - - init(arrowId: String) { - let id = "\(Self.prefix).\(arrowId)" - self.id = id - self.symbolId = "\(id).symbol" - self.arrow = "\(id)" - self.arrowStroke = "\(id).stroke" - self.arrowSymbol = "\(id).symbol" - self.arrowSymbolCasing = "\(id).symbol.casing" - self.arrowSource = "\(id).source" - self.arrowSymbolSource = "\(id).symbol_source" - self.triangleTipImage = "\(id).triangle_tip_image" - } - - static func nextArrow() -> Self { - .init(arrowId: "next") - } - } - - struct VoiceInstruction { - private static let prefix: String = "\(globalPrefix).voice_instruction" - - let featureId: String - let source: String - let layer: String - let circleLayer: String - - init() { - let id = "\(Self.prefix)" - self.featureId = id - self.source = "\(id).source" - self.layer = "\(id).layer" - self.circleLayer = "\(id).layer.circle" - } - - static var currentRoute: Self { - .init() - } - } - - struct IntersectionAnnotation { - private static let prefix: String = "\(globalPrefix).intersection_annotations" - - let featureId: String - let source: String - let layer: String - - let yieldSignImage: String - let stopSignImage: String - let railroadCrossingImage: String - let trafficSignalImage: String - - init() { - let id = "\(Self.prefix)" - self.featureId = id - self.source = "\(id).source" - self.layer = "\(id).layer" - self.yieldSignImage = "\(id).yield_sign" - self.stopSignImage = "\(id).stop_sign" - self.railroadCrossingImage = "\(id).railroad_crossing" - self.trafficSignalImage = "\(id).traffic_signal" - } - - static var currentRoute: Self { - .init() - } - } - - struct RouteAlertAnnotation { - private static let prefix: String = "\(globalPrefix).route_alert_annotations" - - let featureId: String - let source: String - let layer: String - - init() { - let id = "\(Self.prefix)" - self.featureId = id - self.source = "\(id).source" - self.layer = "\(id).layer" - } - - static var `default`: Self { - .init() - } - } - - struct RouteWaypoints { - private static let prefix: String = "\(globalPrefix)_waypoint" - - let featureId: String - let innerCircle: String - let markerIcon: String - let source: String - - init() { - self.featureId = "\(Self.prefix).route-waypoints" - self.innerCircle = "\(Self.prefix).innerCircleLayer" - self.markerIcon = "\(Self.prefix).symbolLayer" - self.source = "\(Self.prefix).source" - } - - static var `default`: Self { - .init() - } - } - - struct RouteAnnotation: Hashable, Sendable { - private static let prefix: String = "\(globalPrefix).route_line.annotation" - let layerId: String - - static var main: Self { - .init(annotationId: "\(prefix).main") - } - - static func alternative(index: Int) -> Self { - .init(annotationId: "\(prefix).alternative_\(index)") - } - - init(annotationId: String) { - self.layerId = annotationId - } - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift deleted file mode 100644 index 559a02d05..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/IntersectionAnnotationsMapFeatures.swift +++ /dev/null @@ -1,142 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxDirections -import MapboxMaps -import enum SwiftUI.ColorScheme -import MapboxNavigationCore - -extension RouteProgress { - func intersectionAnnotationsMapFeatures( - ids: FeatureIds.IntersectionAnnotation, - customizedLayerProvider: CustomizedLayerProvider - ) -> [any MapFeature] { - guard !routeIsComplete else { - return [] - } - - var featureCollection = FeatureCollection(features: []) - - let stepProgress = currentLegProgress.currentStepProgress - let intersectionIndex = stepProgress.intersectionIndex - let intersections = stepProgress.intersectionsIncludingUpcomingManeuverIntersection ?? [] - let stepIntersections = Array(intersections.dropFirst(intersectionIndex)) - - for intersection in stepIntersections { - if let feature = intersectionFeature(from: intersection, ids: ids) { - featureCollection.features.append(feature) - } - } - - let layers: [any Layer] = [ - with(SymbolLayer(id: ids.layer, source: ids.source)) { - $0.iconAllowOverlap = .constant(false) - $0.iconImage = .expression(Exp(.get) { - "imageName" - }) - }, - ] - return [ - GeoJsonMapFeature( - id: ids.featureId, - sources: [ - .init( - id: ids.source, - geoJson: .featureCollection(featureCollection) - ), - ], - customizeSource: { _, _ in }, - layers: layers.map { customizedLayerProvider.customizedLayer($0) }, - onBeforeAdd: { mapView in - Self.upsertIntersectionSymbolImages( - map: mapView.mapboxMap, - ids: ids - ) - }, - onUpdate: { mapView in - Self.upsertIntersectionSymbolImages( - map: mapView.mapboxMap, - ids: ids - ) - }, - onAfterRemove: { mapView in - do { - try Self.removeIntersectionSymbolImages( - map: mapView.mapboxMap, - ids: ids - ) - } catch { - Log.error( - "Failed to remove intersection annotation images with error \(error)", - category: .navigationUI - ) - } - } - ), - ] - } - - private func intersectionFeature( - from intersection: Intersection, - ids: FeatureIds.IntersectionAnnotation - ) -> Feature? { - var properties: JSONObject? - if intersection.yieldSign == true { - properties = ["imageName": .string(ids.yieldSignImage)] - } - if intersection.stopSign == true { - properties = ["imageName": .string(ids.stopSignImage)] - } - if intersection.railroadCrossing == true { - properties = ["imageName": .string(ids.railroadCrossingImage)] - } - if intersection.trafficSignal == true { - properties = ["imageName": .string(ids.trafficSignalImage)] - } - - guard let properties else { return nil } - - var feature = Feature(geometry: .point(Point(intersection.location))) - feature.properties = properties - return feature - } - - public static func resourceBundle() -> Bundle? { - let bundle = Bundle(for: MapboxNavigationProvider.self) - if let resourceBundleURL = bundle.url(forResource: "MapboxNavigationCoreResources", withExtension: "bundle") { - return Bundle(url: resourceBundleURL) - } - return nil - } - - private static func upsertIntersectionSymbolImages( - map: MapboxMap, - ids: FeatureIds.IntersectionAnnotation - ) { - for (imageName, imageIdentifier) in imageNameToMapIdentifier(ids: ids) { - if let image = resourceBundle()?.image(named: imageName) { - map.provisionImage(id: imageIdentifier) { style in - try style.addImage(image, id: imageIdentifier) - } - } - } - } - - private static func removeIntersectionSymbolImages( - map: MapboxMap, - ids: FeatureIds.IntersectionAnnotation - ) throws { - for (_, imageIdentifier) in imageNameToMapIdentifier(ids: ids) { - try map.removeImage(withId: imageIdentifier) - } - } - - private static func imageNameToMapIdentifier( - ids: FeatureIds.IntersectionAnnotation - ) -> [String: String] { - return [ - "TrafficSignal": ids.trafficSignalImage, - "RailroadCrossing": ids.railroadCrossingImage, - "YieldSign": ids.yieldSignImage, - "StopSign": ids.stopSignImage, - ] - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift deleted file mode 100644 index ac4d79bbb..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/ManeuverArrowMapFeatures.swift +++ /dev/null @@ -1,142 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import MapboxDirections -@_spi(Experimental) import MapboxMaps - -extension Route { - - func maneuverArrowMapFeatures( - ids: FeatureIds.ManeuverArrow, - cameraZoom: CGFloat, - legIndex: Int, stepIndex: Int, - config: MapStyleConfig, - customizedLayerProvider: CustomizedLayerProvider - ) -> [any MapFeature] { - guard containsStep(at: legIndex, stepIndex: stepIndex) - else { return [] } - - let bundle = Bundle(for: MapboxNavigationProvider.self) - var moduleBundle: Bundle? = nil - if let resourceBundleURL = bundle.url(forResource: "MapboxNavigationCoreResources", withExtension: "bundle") { - moduleBundle = Bundle(url: resourceBundleURL) - } - - if moduleBundle == nil { - return [] - } - - let triangleImage = moduleBundle!.image(named: "triangle")!.withRenderingMode(.alwaysTemplate) - - var mapFeatures: [any MapFeature] = [] - - let step = legs[legIndex].steps[stepIndex] - let maneuverCoordinate = step.maneuverLocation - guard step.maneuverType != .arrive else { return [] } - - let metersPerPoint = Projection.metersPerPoint( - for: maneuverCoordinate.latitude, - zoom: cameraZoom - ) - - // TODO: Implement ability to change `shaftLength` depending on zoom level. - let shaftLength = max(min(50 * metersPerPoint, 50), 30) - let shaftPolyline = polylineAroundManeuver(legIndex: legIndex, stepIndex: stepIndex, distance: shaftLength) - - if shaftPolyline.coordinates.count > 1 { - let minimumZoomLevel = 14.5 - let shaftStrokeCoordinates = shaftPolyline.coordinates - let shaftDirection = shaftStrokeCoordinates[shaftStrokeCoordinates.count - 2] - .direction(to: shaftStrokeCoordinates.last!) - let point = Point(shaftStrokeCoordinates.last!) - - let layers: [any Layer] = [ - with(LineLayer(id: ids.arrow, source: ids.arrowSource)) { - $0.minZoom = Double(minimumZoomLevel) - $0.lineCap = .constant(.butt) - $0.lineJoin = .constant(.round) - $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.70)) - $0.lineColor = .constant(.init(config.maneuverArrowColor)) - $0.lineEmissiveStrength = .constant(1) - }, - with(LineLayer(id: ids.arrowStroke, source: ids.arrowSource)) { - $0.minZoom = Double(minimumZoomLevel) - $0.lineCap = .constant(.butt) - $0.lineJoin = .constant(.round) - $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.80)) - $0.lineColor = .constant(.init(config.maneuverArrowStrokeColor)) - $0.lineEmissiveStrength = .constant(1) - }, - with(SymbolLayer(id: ids.arrowSymbol, source: ids.arrowSymbolSource)) { - $0.minZoom = Double(minimumZoomLevel) - $0.iconImage = .constant(.name(ids.triangleTipImage)) - $0.iconColor = .constant(.init(config.maneuverArrowColor)) - $0.iconRotationAlignment = .constant(.map) - $0.iconRotate = .constant(.init(shaftDirection)) - $0.iconSize = .expression(Expression.routeLineWidthExpression(0.12)) - $0.iconAllowOverlap = .constant(true) - $0.iconEmissiveStrength = .constant(1) - }, - with(SymbolLayer(id: ids.arrowSymbolCasing, source: ids.arrowSymbolSource)) { - $0.minZoom = Double(minimumZoomLevel) - $0.iconImage = .constant(.name(ids.triangleTipImage)) - $0.iconColor = .constant(.init(config.maneuverArrowStrokeColor)) - $0.iconRotationAlignment = .constant(.map) - $0.iconRotate = .constant(.init(shaftDirection)) - $0.iconSize = .expression(Expression.routeLineWidthExpression(0.14)) - $0.iconAllowOverlap = .constant(true) - }, - ] - - mapFeatures.append( - GeoJsonMapFeature( - id: ids.id, - sources: [ - .init( - id: ids.arrowSource, - geoJson: .feature(Feature(geometry: .lineString(shaftPolyline))) - ), - .init( - id: ids.arrowSymbolSource, - geoJson: .feature(Feature(geometry: .point(point))) - ), - ], - customizeSource: { source, _ in - source.tolerance = 0.375 - }, - layers: layers.map { customizedLayerProvider.customizedLayer($0) }, - onBeforeAdd: { mapView in - mapView.mapboxMap.provisionImage(id: ids.triangleTipImage) { - try $0.addImage( - triangleImage, - id: ids.triangleTipImage, - sdf: true, - stretchX: [], - stretchY: [] - ) - } - }, - onUpdate: { mapView in - try with(mapView.mapboxMap) { - try $0.setLayerProperty( - for: ids.arrowSymbol, - property: "icon-rotate", - value: shaftDirection - ) - try $0.setLayerProperty( - for: ids.arrowSymbolCasing, - property: "icon-rotate", - value: shaftDirection - ) - } - }, - onAfterRemove: { mapView in - if mapView.mapboxMap.imageExists(withId: ids.triangleTipImage) { - try? mapView.mapboxMap.removeImage(withId: ids.triangleTipImage) - } - } - ) - ) - } - return mapFeatures - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift deleted file mode 100644 index 40063dbd1..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAView.swift +++ /dev/null @@ -1,285 +0,0 @@ -import MapboxMaps -import UIKit - -final class ETAView: UIView { - private let label = { - let label = UILabel() - label.textAlignment = .left - return label - }() - - private var tail = UIView() - private let backgroundShape = CAShapeLayer() - private let mapStyleConfig: MapStyleConfig - - let textColor: UIColor - let baloonColor: UIColor - var padding = UIEdgeInsets(allEdges: 10) - var tailSize = 8.0 - var cornerRadius = 8.0 - - var text: String { - didSet { update() } - } - - var anchor: ViewAnnotationAnchor? { - didSet { setNeedsLayout() } - } - - convenience init( - eta: TimeInterval, - isSelected: Bool, - tollsHint: Bool?, - mapStyleConfig: MapStyleConfig - ) { - let viewLabel = DateComponentsFormatter.travelTimeString(eta, signed: false) - - let textColor: UIColor - let baloonColor: UIColor - if isSelected { - textColor = mapStyleConfig.routeAnnotationSelectedTextColor - baloonColor = mapStyleConfig.routeAnnotationSelectedColor - } else { - textColor = mapStyleConfig.routeAnnotationTextColor - baloonColor = mapStyleConfig.routeAnnotationColor - } - - self.init( - text: viewLabel, - tollsHint: tollsHint, - mapStyleConfig: mapStyleConfig, - textColor: textColor, - baloonColor: baloonColor - ) - } - - convenience init( - travelTimeDelta: TimeInterval, - tollsHint: Bool?, - mapStyleConfig: MapStyleConfig - ) { - let textColor: UIColor - let timeDelta: String - if abs(travelTimeDelta) >= 180 { - textColor = if travelTimeDelta > 0 { - mapStyleConfig.routeAnnotationMoreTimeTextColor - } else { - mapStyleConfig.routeAnnotationLessTimeTextColor - } - timeDelta = DateComponentsFormatter.travelTimeString( - travelTimeDelta, - signed: true - ) - } else { - textColor = mapStyleConfig.routeAnnotationTextColor - timeDelta = "SAME_TIME".localizedString( - value: "Similar ETA", - comment: "Alternatives selection note about equal travel time." - ) - } - - self.init( - text: timeDelta, - tollsHint: tollsHint, - mapStyleConfig: mapStyleConfig, - textColor: textColor, - baloonColor: mapStyleConfig.routeAnnotationColor - ) - } - - init( - text: String, - tollsHint: Bool?, - mapStyleConfig: MapStyleConfig, - textColor: UIColor = .darkText, - baloonColor: UIColor = .white - ) { - var viewLabel = text - switch tollsHint { - case .none: - label.numberOfLines = 1 - case .some(true): - label.numberOfLines = 2 - viewLabel += "\n" + "ROUTE_HAS_TOLLS".localizedString( - value: "Tolls", - comment: "Route callout label, indicating there are tolls on the route.") - if let symbol = Locale.current.currencySymbol { - viewLabel += " " + symbol - } - case .some(false): - label.numberOfLines = 2 - viewLabel += "\n" + "ROUTE_HAS_NO_TOLLS".localizedString( - value: "No Tolls", - comment: "Route callout label, indicating there are no tolls on the route.") - } - - self.text = viewLabel - self.textColor = textColor - self.baloonColor = baloonColor - self.mapStyleConfig = mapStyleConfig - super.init(frame: .zero) - layer.addSublayer(backgroundShape) - backgroundShape.shadowRadius = 1.4 - backgroundShape.shadowOffset = CGSize(width: 0, height: 0.7) - backgroundShape.shadowColor = UIColor.black.cgColor - backgroundShape.shadowOpacity = 0.3 - - addSubview(label) - - update() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private var attributedText: NSAttributedString { - let text = NSMutableAttributedString( - attributedString: .labelText( - text, - font: mapStyleConfig.routeAnnotationTextFont, - color: textColor - ) - ) - return text - } - - private func update() { - backgroundShape.fillColor = baloonColor.cgColor - label.attributedText = attributedText - } - - struct Layout { - var label: CGRect - var bubble: CGRect - var size: CGSize - - init(availableSize: CGSize, text: NSAttributedString, tailSize: CGFloat, padding: UIEdgeInsets) { - let tailPadding = UIEdgeInsets(allEdges: tailSize) - - let textPadding = padding + tailPadding + UIEdgeInsets.zero - let textAvailableSize = availableSize - textPadding - let textSize = text.boundingRect( - with: textAvailableSize, - options: .usesLineFragmentOrigin, context: nil - ).size.roundedUp() - self.label = CGRect(padding: textPadding, size: textSize) - self.bubble = CGRect(padding: tailPadding, size: textSize + textPadding - tailPadding) - self.size = bubble.size + tailPadding - } - } - - override func sizeThatFits(_ size: CGSize) -> CGSize { - Layout(availableSize: size, text: attributedText, tailSize: tailSize, padding: padding).size - } - - override func layoutSubviews() { - super.layoutSubviews() - - let layout = Layout(availableSize: bounds.size, text: attributedText, tailSize: tailSize, padding: padding) - label.frame = layout.label - - let calloutPath = UIBezierPath.calloutPath( - size: bounds.size, - tailSize: tailSize, - cornerRadius: cornerRadius, - anchor: anchor ?? .center - ) - backgroundShape.path = calloutPath.cgPath - backgroundShape.frame = bounds - } -} - -extension UIEdgeInsets { - fileprivate init(allEdges value: CGFloat) { - self.init(top: value, left: value, bottom: value, right: value) - } -} - -extension NSAttributedString { - fileprivate static func labelText(_ string: String, font: UIFont, color: UIColor) -> NSAttributedString { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .left - let attributes = [ - NSAttributedString.Key.paragraphStyle: paragraphStyle, - .font: font, - .foregroundColor: color, - ] - return NSAttributedString(string: string, attributes: attributes) - } -} - -extension CGSize { - fileprivate func roundedUp() -> CGSize { - CGSize(width: width.rounded(.up), height: height.rounded(.up)) - } -} - -extension CGRect { - fileprivate init(padding: UIEdgeInsets, size: CGSize) { - self.init(origin: CGPoint(x: padding.left, y: padding.top), size: size) - } -} - -private func + (lhs: CGSize, rhs: UIEdgeInsets) -> CGSize { - return CGSize(width: lhs.width + rhs.left + rhs.right, height: lhs.height + rhs.top + rhs.bottom) -} - -private func - (lhs: CGSize, rhs: UIEdgeInsets) -> CGSize { - return CGSize(width: lhs.width - rhs.left - rhs.right, height: lhs.height - rhs.top - rhs.bottom) -} - -extension UIBezierPath { - fileprivate static func calloutPath( - size: CGSize, - tailSize: CGFloat, - cornerRadius: CGFloat, - anchor: ViewAnnotationAnchor - ) -> UIBezierPath { - let rect = CGRect(origin: .init(x: 0, y: 0), size: size) - let bubbleRect = rect.insetBy(dx: tailSize, dy: tailSize) - - let path = UIBezierPath( - roundedRect: bubbleRect, - cornerRadius: cornerRadius - ) - - let tailPath = UIBezierPath() - let p = tailSize - let h = size.height - let w = size.width - let r = cornerRadius - let tailPoints: [CGPoint] = switch anchor { - case .topLeft: - [CGPoint(x: 0, y: 0), CGPoint(x: p + r, y: p), CGPoint(x: p, y: p + r)] - case .top: - [CGPoint(x: w / 2, y: 0), CGPoint(x: w / 2 - p, y: p), CGPoint(x: w / 2 + p, y: p)] - case .topRight: - [CGPoint(x: w, y: 0), CGPoint(x: w - p, y: p + r), CGPoint(x: w - 3 * p, y: p)] - case .bottomLeft: - [CGPoint(x: 0, y: h), CGPoint(x: p, y: h - (p + r)), CGPoint(x: p + r, y: h - p)] - case .bottom: - [CGPoint(x: w / 2, y: h), CGPoint(x: w / 2 - p, y: h - p), CGPoint(x: w / 2 + p, y: h - p)] - case .bottomRight: - [CGPoint(x: w, y: h), CGPoint(x: w - (p + r), y: h - p), CGPoint(x: w - p, y: h - (p + r))] - case .left: - [CGPoint(x: 0, y: h / 2), CGPoint(x: p, y: h / 2 - p), CGPoint(x: p, y: h / 2 + p)] - case .right: - [CGPoint(x: w, y: h / 2), CGPoint(x: w - p, y: h / 2 - p), CGPoint(x: w - p, y: h / 2 + p)] - default: - [] - } - - for (i, point) in tailPoints.enumerated() { - if i == 0 { - tailPath.move(to: point) - } else { - tailPath.addLine(to: point) - } - } - tailPath.close() - path.append(tailPath) - return path - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift deleted file mode 100644 index 75d9b5c17..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/ETAViewsAnnotationFeature.swift +++ /dev/null @@ -1,139 +0,0 @@ -import Foundation -import MapboxDirections -import MapboxMaps - -struct ETAViewsAnnotationFeature: MapFeature { - var id: String - - private let viewAnnotations: [ViewAnnotation] - - init( - for navigationRoutes: NavigationRoutes, - showMainRoute: Bool, - showAlternatives: Bool, - isRelative: Bool, - annotateAtManeuver: Bool, - mapStyleConfig: MapStyleConfig - ) { - let routesContainTolls = navigationRoutes.alternativeRoutes.contains { - ($0.route.tollIntersections?.count ?? 0) > 0 - } - var featureId = "" - - var annotations = [ViewAnnotation]() - if showMainRoute { - featureId += navigationRoutes.mainRoute.routeId.rawValue - let tollsHint = routesContainTolls ? navigationRoutes.mainRoute.route.containsTolls : nil - let etaView = ETAView( - eta: navigationRoutes.mainRoute.route.expectedTravelTime, - isSelected: true, - tollsHint: tollsHint, - mapStyleConfig: mapStyleConfig - ) - if let geometry = navigationRoutes.mainRoute.route.geometryForCallout() { - annotations.append( - ViewAnnotation( - annotatedFeature: .geometry(geometry), - view: etaView - ) - ) - } else { - annotations.append( - ViewAnnotation( - layerId: FeatureIds.RouteAnnotation.main.layerId, - view: etaView - ) - ) - } - } - if showAlternatives { - for (idx, alternativeRoute) in navigationRoutes.alternativeRoutes.enumerated() { - featureId += alternativeRoute.routeId.rawValue - let tollsHint = routesContainTolls ? alternativeRoute.route.containsTolls : nil - let etaView = if isRelative { - ETAView( - travelTimeDelta: alternativeRoute.expectedTravelTimeDelta, - tollsHint: tollsHint, - mapStyleConfig: mapStyleConfig - ) - } else { - ETAView( - eta: alternativeRoute.infoFromOrigin.duration, - isSelected: false, - tollsHint: tollsHint, - mapStyleConfig: mapStyleConfig - ) - } - let limit: Range - if annotateAtManeuver { - let deviationOffset = alternativeRoute.deviationOffset() - limit = (deviationOffset + 0.01)..<(deviationOffset + 0.05) - } else { - limit = 0.2..<0.8 - } - if let geometry = alternativeRoute.route.geometryForCallout(clampedTo: limit) { - annotations.append( - ViewAnnotation( - annotatedFeature: .geometry(geometry), - view: etaView - ) - ) - } else { - annotations.append( - ViewAnnotation( - layerId: FeatureIds.RouteAnnotation.alternative(index: idx).layerId, - view: etaView - ) - ) - } - } - } - annotations.forEach { - guard let etaView = $0.view as? ETAView else { return } - $0.setup(with: etaView) - } - self.id = featureId - self.viewAnnotations = annotations - } - - func add(to mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { - for annotation in viewAnnotations { - mapView.viewAnnotations.add(annotation) - } - } - - func remove(from mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { - viewAnnotations.forEach { $0.remove() } - } - - func update(oldValue: any MapFeature, in mapView: MapboxMaps.MapView, order: inout MapLayersOrder) { - oldValue.remove(from: mapView, order: &order) - add(to: mapView, order: &order) - } -} - -extension Route { - fileprivate func geometryForCallout(clampedTo range: Range = 0.2..<0.8) -> Geometry? { - return shape?.trimmed( - from: distance * range.lowerBound, - to: distance * range.upperBound - )?.geometry - } - - fileprivate var containsTolls: Bool { - !(tollIntersections?.isEmpty ?? true) - } -} - -extension ViewAnnotation { - fileprivate func setup(with etaView: ETAView) { - ignoreCameraPadding = true - onAnchorChanged = { config in - etaView.anchor = config.anchor - } - variableAnchors = [ViewAnnotationAnchor.bottomLeft, .bottomRight, .topLeft, .topRight].map { - ViewAnnotationAnchorConfig(anchor: $0) - } - setNeedsUpdateSize() - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift deleted file mode 100644 index 2ddd6d67f..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/GeoJsonMapFeature.swift +++ /dev/null @@ -1,239 +0,0 @@ -import Foundation -import MapboxMaps -import Turf - -/// Simplifies source/layer/image managements for MapView -/// -/// ## Supported features: -/// -/// ### Layers -/// -/// Can be added/removed but not updated. Custom update logic can be performed using `onUpdate` callback. This -/// is done for performance reasons and to simplify implementation as map layers doesn't support equatable protocol. -/// If you want to update layers, you can consider assigning updated layer a new id. -/// -/// It there is only one source, layers will get it assigned automatically, overwise, layers should has source set -/// manually. -/// -/// ### Sources -/// -/// Sources can also be added/removed, but unlike layers, sources are always updated. -/// -/// -struct GeoJsonMapFeature: MapFeature { - struct Source { - let id: String - let geoJson: GeoJSONObject - } - - typealias LayerId = String - typealias SourceId = String - - let id: String - let sources: [SourceId: Source] - - let customizeSource: @MainActor (_ source: inout GeoJSONSource, _ id: String) -> Void - - let layers: [LayerId: any Layer] - - // MARK: Lifecycle callbacks - - let onBeforeAdd: @MainActor (_ mapView: MapView) -> Void - let onAfterAdd: @MainActor (_ mapView: MapView) -> Void - let onUpdate: @MainActor (_ mapView: MapView) throws -> Void - let onAfterUpdate: @MainActor (_ mapView: MapView) throws -> Void - let onAfterRemove: @MainActor (_ mapView: MapView) -> Void - - init( - id: String, - sources: [Source], - customizeSource: @escaping @MainActor (_: inout GeoJSONSource, _ id: String) -> Void, - layers: [any Layer], - onBeforeAdd: @escaping @MainActor (_: MapView) -> Void = { _ in }, - onAfterAdd: @escaping @MainActor (_: MapView) -> Void = { _ in }, - onUpdate: @escaping @MainActor (_: MapView) throws -> Void = { _ in }, - onAfterUpdate: @escaping @MainActor (_: MapView) throws -> Void = { _ in }, - onAfterRemove: @escaping @MainActor (_: MapView) -> Void = { _ in } - ) { - self.id = id - self.sources = Dictionary(uniqueKeysWithValues: sources.map { ($0.id, $0) }) - self.customizeSource = customizeSource - self.layers = Dictionary(uniqueKeysWithValues: layers.map { ($0.id, $0) }) - self.onBeforeAdd = onBeforeAdd - self.onAfterAdd = onAfterAdd - self.onUpdate = onUpdate - self.onAfterUpdate = onAfterUpdate - self.onAfterRemove = onAfterRemove - } - - // MARK: - MapFeature conformance - - @MainActor - func add(to mapView: MapView, order: inout MapLayersOrder) { - onBeforeAdd(mapView) - - let map: MapboxMap = mapView.mapboxMap - for (_, source) in sources { - addSource(source, to: map) - } - - for (_, var layer) in layers { - addLayer(&layer, to: map, order: &order) - } - - onAfterAdd(mapView) - } - - @MainActor - private func addLayer(_ layer: inout any Layer, to map: MapboxMap, order: inout MapLayersOrder) { - do { - if map.layerExists(withId: layer.id) { - try map.removeLayer(withId: layer.id) - } - order.insert(id: layer.id) - if let slot = order.slot(forId: layer.id), map.allSlotIdentifiers.contains(slot) { - layer.slot = slot - } - try map.addLayer(layer, layerPosition: order.position(forId: layer.id)) - } catch { - Log.error("Failed to add layer '\(layer.id)': \(error)", category: .navigationUI) - } - } - - @MainActor - private func addSource(_ source: Source, to map: MapboxMap) { - do { - if map.sourceExists(withId: source.id) { - map.updateGeoJSONSource( - withId: source.id, - geoJSON: source.geoJson - ) - } else { - var geoJsonSource = GeoJSONSource(id: source.id) - geoJsonSource.data = source.geoJson.sourceData - customizeSource(&geoJsonSource, source.id) - try map.addSource(geoJsonSource) - } - } catch { - Log.error("Failed to add source '\(source.id)': \(error)", category: .navigationUI) - } - } - - @MainActor - func update(oldValue: any MapFeature, in mapView: MapView, order: inout MapLayersOrder) { - guard let oldValue = oldValue as? Self else { - preconditionFailure("Incorrect type passed for oldValue") - } - - for (_, source) in sources { - guard mapView.mapboxMap.sourceExists(withId: source.id) - else { - // In case the map style was changed and the source is missing we're re-adding it back. - oldValue.remove(from: mapView, order: &order) - remove(from: mapView, order: &order) - add(to: mapView, order: &order) - return - } - } - - do { - try onUpdate(mapView) - let map: MapboxMap = mapView.mapboxMap - - let diff = diff(oldValue: oldValue, newValue: self) - for var addedLayer in diff.addedLayers { - addLayer(&addedLayer, to: map, order: &order) - } - for removedLayer in diff.removedLayers { - removeLayer(removedLayer, from: map, order: &order) - } - for addedSource in diff.addedSources { - addSource(addedSource, to: map) - } - for removedSource in diff.removedSources { - removeSource(removedSource.id, from: map) - } - - for (_, source) in sources { - mapView.mapboxMap.updateGeoJSONSource( - withId: source.id, - geoJSON: source.geoJson - ) - } - try onAfterUpdate(mapView) - } catch { - Log.error("Failed to update map feature '\(id)': \(error)", category: .navigationUI) - } - } - - @MainActor - func remove(from mapView: MapView, order: inout MapLayersOrder) { - let map: MapboxMap = mapView.mapboxMap - - for (_, layer) in layers { - removeLayer(layer, from: map, order: &order) - } - - for sourceId in sources.keys { - removeSource(sourceId, from: map) - } - - onAfterRemove(mapView) - } - - @MainActor - private func removeLayer(_ layer: any Layer, from map: MapboxMap, order: inout MapLayersOrder) { - guard map.layerExists(withId: layer.id) else { return } - do { - try map.removeLayer(withId: layer.id) - order.remove(id: layer.id) - } catch { - Log.error("Failed to remove layer '\(layer.id)': \(error)", category: .navigationUI) - } - } - - @MainActor - private func removeSource(_ sourceId: SourceId, from map: MapboxMap) { - if map.sourceExists(withId: sourceId) { - do { - try map.removeSource(withId: sourceId) - } catch { - Log.error("Failed to remove source '\(sourceId)': \(error)", category: .navigationUI) - } - } - } - - // MARK: Diff - - private struct Diff { - let addedLayers: [any Layer] - let removedLayers: [any Layer] - let addedSources: [Source] - let removedSources: [Source] - } - - private func diff(oldValue: Self, newValue: Self) -> Diff { - .init( - addedLayers: newValue.layers.filter { oldValue.layers[$0.key] == nil }.map(\.value), - removedLayers: oldValue.layers.filter { newValue.layers[$0.key] == nil }.map(\.value), - addedSources: newValue.sources.filter { oldValue.sources[$0.key] == nil }.map(\.value), - removedSources: oldValue.sources.filter { newValue.sources[$0.key] == nil }.map(\.value) - ) - } -} - -// MARK: Helpers - -extension GeoJSONObject { - /// Ported from MapboxMaps as the same var is internal in the SDK. - fileprivate var sourceData: GeoJSONSourceData { - switch self { - case .geometry(let geometry): - return .geometry(geometry) - case .feature(let feature): - return .feature(feature) - case .featureCollection(let collection): - return .featureCollection(collection) - } - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift deleted file mode 100644 index 36263f283..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeature.swift +++ /dev/null @@ -1,16 +0,0 @@ -// import Foundation -// import MapboxMaps - -// /// Something that can be added/removed/updated in MapboxMaps.MapView. -// /// -// /// Use ``MapFeaturesStore`` to manage a set of features. -// protocol MapFeature { -// var id: String { get } - -// @MainActor -// func add(to mapView: MapView, order: inout MapLayersOrder) -// @MainActor -// func remove(from mapView: MapView, order: inout MapLayersOrder) -// @MainActor -// func update(oldValue: MapFeature, in mapView: MapView, order: inout MapLayersOrder) -// } diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift deleted file mode 100644 index 1f5cb49fb..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/MapFeaturesStore.swift +++ /dev/null @@ -1,117 +0,0 @@ -import Foundation -import MapboxMaps - -/// A store for ``MapFeature``s. -/// -/// It handle style reload by re-adding currently active features (make sure you call `styleLoaded` method). -/// Use `update(using:)` method to provide a new snapshot of features that are managed by this store. The store will -/// handle updates/removes/additions to the map view. -@MainActor -final class MapFeaturesStore { - private struct Features: Sequence { - private var features: [String: any MapFeature] = [:] - - func makeIterator() -> some IteratorProtocol { - features.values.makeIterator() - } - - subscript(_ id: String) -> (any MapFeature)? { - features[id] - } - - mutating func insert(_ feature: any MapFeature) { - features[feature.id] = feature - } - - mutating func remove(_ feature: any MapFeature) -> (any MapFeature)? { - features.removeValue(forKey: feature.id) - } - - mutating func removeAll() -> some Sequence { - let allFeatures = features.values - features = [:] - return allFeatures - } - } - - private let mapView: MapView - private var styleLoadSubscription: MapboxMaps.Cancelable? - private var features: Features = .init() - - private var currentStyleLoaded: Bool = false - private var currentStyleUri: StyleURI? - - private var styleLoaded: Bool { - if currentStyleUri != mapView.mapboxMap.styleURI { - currentStyleLoaded = false - } - return currentStyleLoaded - } - - init(mapView: MapView) { - self.mapView = mapView - self.currentStyleUri = mapView.mapboxMap.styleURI - self.currentStyleLoaded = mapView.mapboxMap.isStyleLoaded - } - - func deactivate(order: inout MapLayersOrder) { - styleLoadSubscription?.cancel() - guard styleLoaded else { return } - features.forEach { $0.remove(from: mapView, order: &order) } - } - - func update(using allFeatures: [any MapFeature]?, order: inout MapLayersOrder) { - guard let allFeatures, !allFeatures.isEmpty else { - removeAll(order: &order); return - } - - let newFeatureIds = Set(allFeatures.map(\.id)) - for existingFeature in features where !newFeatureIds.contains(existingFeature.id) { - remove(existingFeature, order: &order) - } - - for feature in allFeatures { - update(feature, order: &order) - } - } - - private func removeAll(order: inout MapLayersOrder) { - let allFeatures = features.removeAll() - guard styleLoaded else { return } - - for feature in allFeatures { - feature.remove(from: mapView, order: &order) - } - } - - private func update(_ feature: any MapFeature, order: inout MapLayersOrder) { - defer { - features.insert(feature) - } - - guard styleLoaded else { return } - - if let oldFeature = features[feature.id] { - feature.update(oldValue: oldFeature, in: mapView, order: &order) - } else { - feature.add(to: mapView, order: &order) - } - } - - private func remove(_ feature: some MapFeature, order: inout MapLayersOrder) { - guard let removeFeature = features.remove(feature) else { return } - - if styleLoaded { - removeFeature.remove(from: mapView, order: &order) - } - } - - func styleLoaded(order: inout MapLayersOrder) { - currentStyleUri = mapView.mapboxMap.styleURI - currentStyleLoaded = true - - for feature in features { - feature.add(to: mapView, order: &order) - } - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift deleted file mode 100644 index 830f23d92..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/MapFeatures/Style++.swift +++ /dev/null @@ -1,38 +0,0 @@ -import MapboxMaps - -extension MapboxMap { - /// Adds image to style if it doesn't exist already and log any errors that occur. - func provisionImage(id: String, _ addImageToMap: (MapboxMap) throws -> Void) { - if !imageExists(withId: id) { - do { - try addImageToMap(self) - } catch { - Log.error("Failed to add image (id: \(id)) to style with error \(error)", category: .navigationUI) - } - } - } - - func setRouteLineOffset( - _ offset: Double, - for routeLineIds: FeatureIds.RouteLine - ) { - guard offset >= 0.0 else { return } - do { - let layerIds: [String] = [ - routeLineIds.main, - routeLineIds.casing, - routeLineIds.restrictedArea, - ] - - for layerId in layerIds where layerExists(withId: layerId) { - try setLayerProperty( - for: layerId, - property: "line-trim-offset", - value: [0.0, Double.minimum(1.0, offset)] - ) - } - } catch { - Log.error("Failed to update route line gradient with error: \(error)", category: .navigationUI) - } - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/MapLayersOrder.swift b/ios/Classes/MapboxNavigationCore/Map/Style/MapLayersOrder.swift deleted file mode 100644 index bdb23e842..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/MapLayersOrder.swift +++ /dev/null @@ -1,260 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxMaps - -/// Allows to order layers with easy by defining order rules and then query order for any added layer. -struct MapLayersOrder { - @resultBuilder - enum Builder { - static func buildPartialBlock(first rule: Rule) -> [Rule] { - [rule] - } - - static func buildPartialBlock(first slottedRules: SlottedRules) -> [Rule] { - slottedRules.rules - } - - static func buildPartialBlock(accumulated rules: [Rule], next rule: Rule) -> [Rule] { - with(rules) { - $0.append(rule) - } - } - - static func buildPartialBlock(accumulated rules: [Rule], next slottedRules: SlottedRules) -> [Rule] { - rules + slottedRules.rules - } - } - - struct SlottedRules { - let rules: [MapLayersOrder.Rule] - - init(_ slot: Slot?, @MapLayersOrder.Builder rules: () -> [Rule]) { - self.rules = rules().map { rule in - with(rule) { $0.slot = slot } - } - } - } - - struct Rule { - struct MatchPredicate { - let block: (String) -> Bool - - static func hasPrefix(_ prefix: String) -> Self { - .init { - $0.hasPrefix(prefix) - } - } - - static func contains(_ substring: String) -> Self { - .init { - $0.contains(substring) - } - } - - static func exact(_ id: String) -> Self { - .init { - $0 == id - } - } - - static func any(of ids: any Sequence) -> Self { - let set = Set(ids) - return .init { - set.contains($0) - } - } - } - - struct OrderedAscendingComparator { - let block: (_ lhs: String, _ rhs: String) -> Bool - - static func constant(_ value: Bool) -> Self { - .init { _, _ in - value - } - } - - static func order(_ ids: [String]) -> Self { - return .init { lhs, rhs in - guard let lhsIndex = ids.firstIndex(of: lhs), - let rhsIndex = ids.firstIndex(of: rhs) else { return true } - return lhsIndex < rhsIndex - } - } - } - - let matches: (String) -> Bool - let isOrderedAscending: (_ lhs: String, _ rhs: String) -> Bool - var slot: Slot? - - init( - predicate: MatchPredicate, - isOrderedAscending: OrderedAscendingComparator - ) { - self.matches = predicate.block - self.isOrderedAscending = isOrderedAscending.block - } - - static func hasPrefix( - _ prefix: String, - isOrderedAscending: OrderedAscendingComparator = .constant(true) - ) -> Rule { - Rule(predicate: .hasPrefix(prefix), isOrderedAscending: isOrderedAscending) - } - - static func contains( - _ substring: String, - isOrderedAscending: OrderedAscendingComparator = .constant(true) - ) -> Rule { - Rule(predicate: .contains(substring), isOrderedAscending: isOrderedAscending) - } - - static func exact( - _ id: String, - isOrderedAscending: OrderedAscendingComparator = .constant(true) - ) -> Rule { - Rule(predicate: .exact(id), isOrderedAscending: isOrderedAscending) - } - - static func orderedIds(_ ids: [String]) -> Rule { - return Rule( - predicate: .any(of: ids), - isOrderedAscending: .order(ids) - ) - } - - func slotted(_ slot: Slot) -> Self { - with(self) { - $0.slot = slot - } - } - } - - /// Ids that are managed by map style. - private var styleIds: [String] = [] - /// Ids that are managed by SDK. - private var customIds: Set = [] - /// Merged `styleIds` and `customIds` in order defined by rules. - private var orderedIds: [String] = [] - /// A map from id to position in `orderedIds` to speed up `position(forId:)` query. - private var orderedIdsIndices: [String: Int] = [:] - private var idToSlot: [String: Slot] = [:] - /// Ordered list of rules that define order. - private let rules: [Rule] - - /// Used for styles with no slots support. - private let legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? - - init( - @MapLayersOrder.Builder builder: () -> [Rule], - legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? - ) { - self.rules = builder() - self.legacyPosition = legacyPosition - } - - /// Inserts a new id and makes it possible to use it in `position(forId:)` method. - mutating func insert(id: String) { - customIds.insert(id) - - guard let ruleIndex = rules.firstIndex(where: { $0.matches(id) }) else { - orderedIds.append(id) - orderedIdsIndices[id] = orderedIds.count - 1 - return - } - - func binarySearch() -> Int { - var left = 0 - var right = orderedIds.count - - while left < right { - let mid = left + (right - left) / 2 - if let currentRuleIndex = rules.firstIndex(where: { $0.matches(orderedIds[mid]) }) { - if currentRuleIndex > ruleIndex { - right = mid - } else if currentRuleIndex == ruleIndex { - if !rules[ruleIndex].isOrderedAscending(orderedIds[mid], id) { - right = mid - } else { - left = mid + 1 - } - } else { - left = mid + 1 - } - } else { - right = mid - } - } - return left - } - - let insertionIndex = binarySearch() - orderedIds.insert(id, at: insertionIndex) - - // Update the indices of the elements after the insertion point - for index in insertionIndex.. LayerPosition? { - if let legacyPosition { - return legacyPosition(id) - } - - guard let index = orderedIdsIndices[id] else { return nil } - let belowId = index == 0 ? nil : orderedIds[index - 1] - let aboveId = index == orderedIds.count - 1 ? nil : orderedIds[index + 1] - - if let belowId { - return .above(belowId) - } else if let aboveId { - return .below(aboveId) - } else { - return nil - } - } - - func slot(forId id: String) -> Slot? { - idToSlot[id] - } - - private func rule(matching id: String) -> Rule? { - rules.first { rule in - rule.matches(id) - } - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift b/ios/Classes/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift deleted file mode 100644 index 30601ca7f..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/NavigationMapStyleManager.swift +++ /dev/null @@ -1,538 +0,0 @@ -import Combine -import MapboxDirections -@_spi(Experimental) import MapboxMaps -import enum SwiftUI.ColorScheme -import UIKit -import MapboxNavigationCore - -struct CustomizedLayerProvider { - var customizedLayer: (Layer) -> Layer -} - -struct MapStyleConfig: Equatable { - var routeCasingColor: UIColor - var routeAlternateCasingColor: UIColor - var routeRestrictedAreaColor: UIColor - var traversedRouteColor: UIColor? - var maneuverArrowColor: UIColor - var maneuverArrowStrokeColor: UIColor - - var routeAnnotationSelectedColor: UIColor - var routeAnnotationColor: UIColor - var routeAnnotationSelectedTextColor: UIColor - var routeAnnotationTextColor: UIColor - var routeAnnotationMoreTimeTextColor: UIColor - var routeAnnotationLessTimeTextColor: UIColor - var routeAnnotationTextFont: UIFont - - var routeLineTracksTraversal: Bool - var isRestrictedAreaEnabled: Bool - var showsTrafficOnRouteLine: Bool - var showsAlternatives: Bool - var showsIntermediateWaypoints: Bool - var occlusionFactor: Value? - var congestionConfiguration: CongestionConfiguration - - var waypointColor: UIColor - var waypointStrokeColor: UIColor -} - -/// Manages all the sources/layers used in NavigationMap. -@MainActor -final class NavigationMapStyleManager { - private let mapView: MapView - private var lifetimeSubscriptions: Set = [] - private var layersOrder: MapLayersOrder - private var layerIds: [String] - - var customizedLayerProvider: CustomizedLayerProvider = .init { $0 } - var customRouteLineLayerPosition: MapboxMaps.LayerPosition? - - private let routeFeaturesStore: MapFeaturesStore - private let waypointFeaturesStore: MapFeaturesStore - private let arrowFeaturesStore: MapFeaturesStore - private let voiceInstructionFeaturesStore: MapFeaturesStore - private let intersectionAnnotationsFeaturesStore: MapFeaturesStore - private let routeAnnotationsFeaturesStore: MapFeaturesStore - private let routeAlertsFeaturesStore: MapFeaturesStore - - init(mapView: MapView, customRouteLineLayerPosition: MapboxMaps.LayerPosition?) { - self.mapView = mapView - self.layersOrder = Self.makeMapLayersOrder( - with: mapView, - customRouteLineLayerPosition: customRouteLineLayerPosition - ) - self.layerIds = mapView.mapboxMap.allLayerIdentifiers.map(\.id) - self.routeFeaturesStore = .init(mapView: mapView) - self.waypointFeaturesStore = .init(mapView: mapView) - self.arrowFeaturesStore = .init(mapView: mapView) - self.voiceInstructionFeaturesStore = .init(mapView: mapView) - self.intersectionAnnotationsFeaturesStore = .init(mapView: mapView) - self.routeAnnotationsFeaturesStore = .init(mapView: mapView) - self.routeAlertsFeaturesStore = .init(mapView: mapView) - - mapView.mapboxMap.onStyleLoaded.sink { [weak self] _ in - self?.onStyleLoaded() - }.store(in: &lifetimeSubscriptions) - } - - func onStyleLoaded() { - // MapsSDK removes all layers when a style is loaded, so we have to recreate MapLayersOrder. - layersOrder = Self.makeMapLayersOrder(with: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) - layerIds = mapView.mapboxMap.allLayerIdentifiers.map(\.id) - layersOrder.setStyleIds(layerIds) - - routeFeaturesStore.styleLoaded(order: &layersOrder) - waypointFeaturesStore.styleLoaded(order: &layersOrder) - arrowFeaturesStore.styleLoaded(order: &layersOrder) - voiceInstructionFeaturesStore.styleLoaded(order: &layersOrder) - intersectionAnnotationsFeaturesStore.styleLoaded(order: &layersOrder) - routeAnnotationsFeaturesStore.styleLoaded(order: &layersOrder) - routeAlertsFeaturesStore.styleLoaded(order: &layersOrder) - } - - func updateRoutes( - _ routes: NavigationRoutes, - config: MapStyleConfig, - featureProvider: RouteLineFeatureProvider - ) { - routeFeaturesStore.update( - using: routeLineMapFeatures( - routes: routes, - config: config, - featureProvider: featureProvider, - customizedLayerProvider: customizedLayerProvider - ), - order: &layersOrder - ) - } - - func updateWaypoints( - route: Route, - legIndex: Int, - config: MapStyleConfig, - featureProvider: WaypointFeatureProvider - ) { - let waypoints = route.waypointsMapFeature( - mapView: mapView, - legIndex: legIndex, - config: config, - featureProvider: featureProvider, - customizedLayerProvider: customizedLayerProvider - ) - waypointFeaturesStore.update( - using: waypoints.map { [$0] } ?? [], - order: &layersOrder - ) - } - - func updateArrows( - route: Route, - legIndex: Int, - stepIndex: Int, - config: MapStyleConfig - ) { - guard route.containsStep(at: legIndex, stepIndex: stepIndex) - else { - removeArrows(); return - } - - arrowFeaturesStore.update( - using: route.maneuverArrowMapFeatures( - ids: .nextArrow(), - cameraZoom: mapView.mapboxMap.cameraState.zoom, - legIndex: legIndex, - stepIndex: stepIndex, - config: config, - customizedLayerProvider: customizedLayerProvider - ), - order: &layersOrder - ) - } - - func updateVoiceInstructions(route: Route) { - voiceInstructionFeaturesStore.update( - using: route.voiceInstructionMapFeatures( - ids: .init(), - customizedLayerProvider: customizedLayerProvider - ), - order: &layersOrder - ) - } - - func updateIntersectionAnnotations(routeProgress: MapboxNavigationCore.RouteProgress) { - intersectionAnnotationsFeaturesStore.update( - using: routeProgress.intersectionAnnotationsMapFeatures( - ids: .currentRoute, - customizedLayerProvider: customizedLayerProvider - ), - order: &layersOrder - ) - } - - func updateRouteAnnotations( - navigationRoutes: MapboxNavigationCore.NavigationRoutes, - annotationKinds: Set, - config: MapStyleConfig - ) { - routeAnnotationsFeaturesStore.update( - using: navigationRoutes.routeDurationMapFeatures( - annotationKinds: annotationKinds, - config: config - ), - order: &layersOrder - ) - } - - func updateRouteAlertsAnnotations( - navigationRoutes: MapboxNavigationCore.NavigationRoutes, - excludedRouteAlertTypes: RoadAlertType, - distanceTraveled: CLLocationDistance = 0.0 - ) { - routeAlertsFeaturesStore.update( - using: navigationRoutes.routeAlertsAnnotationsMapFeatures( - ids: .default, - distanceTraveled: distanceTraveled, - customizedLayerProvider: customizedLayerProvider, - excludedRouteAlertTypes: excludedRouteAlertTypes - ), - order: &layersOrder - ) - } - - func updateFreeDriveAlertsAnnotations( - roadObjects: [RoadObjectAhead], - excludedRouteAlertTypes: RoadAlertType, - distanceTraveled: CLLocationDistance = 0.0 - ) { - guard !roadObjects.isEmpty else { - return removeRoadAlertsAnnotations() - } - routeAlertsFeaturesStore.update( - using: roadObjects.routeAlertsAnnotationsMapFeatures( - ids: .default, - distanceTraveled: distanceTraveled, - customizedLayerProvider: customizedLayerProvider, - excludedRouteAlertTypes: excludedRouteAlertTypes - ), - order: &layersOrder - ) - } - - func removeRoutes() { - routeFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeWaypoints() { - waypointFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeArrows() { - arrowFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeVoiceInstructions() { - voiceInstructionFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeIntersectionAnnotations() { - intersectionAnnotationsFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeRouteAnnotations() { - routeAnnotationsFeaturesStore.update(using: nil, order: &layersOrder) - } - - private func removeRoadAlertsAnnotations() { - routeAlertsFeaturesStore.update(using: nil, order: &layersOrder) - } - - func removeAllFeatures() { - removeRoutes() - removeWaypoints() - removeArrows() - removeVoiceInstructions() - removeIntersectionAnnotations() - removeRouteAnnotations() - removeRoadAlertsAnnotations() - } - - private func routeLineMapFeatures( - routes: NavigationRoutes, - config: MapStyleConfig, - featureProvider: RouteLineFeatureProvider, - customizedLayerProvider: CustomizedLayerProvider - ) -> [any MapFeature] { - var features: [any MapFeature] = [] - - features.append(contentsOf: routes.mainRoute.route.routeLineMapFeatures( - ids: .main, - offset: 0, - isSoftGradient: true, - isAlternative: false, - config: config, - featureProvider: featureProvider, - customizedLayerProvider: customizedLayerProvider - )) - - if config.showsAlternatives { - for (idx, alternativeRoute) in routes.alternativeRoutes.enumerated() { - let deviationOffset = alternativeRoute.deviationOffset() - features.append(contentsOf: alternativeRoute.route.routeLineMapFeatures( - ids: .alternative(idx: idx), - offset: deviationOffset, - isSoftGradient: true, - isAlternative: true, - config: config, - featureProvider: featureProvider, - customizedLayerProvider: customizedLayerProvider - )) - } - } - - return features - } - - func setRouteLineOffset( - _ offset: Double, - for routeLineIds: FeatureIds.RouteLine - ) { - mapView.mapboxMap.setRouteLineOffset(offset, for: routeLineIds) - } - - private static func makeMapLayersOrder( - with mapView: MapView, - customRouteLineLayerPosition: MapboxMaps.LayerPosition? - ) -> MapLayersOrder { - let alternative_0_ids = FeatureIds.RouteLine.alternative(idx: 0) - let alternative_1_ids = FeatureIds.RouteLine.alternative(idx: 1) - let mainLineIds = FeatureIds.RouteLine.main - let arrowIds = FeatureIds.ManeuverArrow.nextArrow() - let waypointIds = FeatureIds.RouteWaypoints.default - let voiceInstructionIds = FeatureIds.VoiceInstruction.currentRoute - let intersectionIds = FeatureIds.IntersectionAnnotation.currentRoute - let routeAlertIds = FeatureIds.RouteAlertAnnotation.default - typealias R = MapLayersOrder.Rule - typealias SlottedRules = MapLayersOrder.SlottedRules - - let allSlotIdentifiers = mapView.mapboxMap.allSlotIdentifiers - let containsMiddleSlot = Slot.middle.map(allSlotIdentifiers.contains) ?? false - let legacyPosition: ((String) -> MapboxMaps.LayerPosition?)? = containsMiddleSlot ? nil : { - legacyLayerPosition(for: $0, mapView: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) - } - - return MapLayersOrder( - builder: { - SlottedRules(.middle) { - R.orderedIds([ - alternative_0_ids.casing, - alternative_0_ids.main, - ]) - R.orderedIds([ - alternative_1_ids.casing, - alternative_1_ids.main, - ]) - R.orderedIds([ - mainLineIds.traversedRoute, - mainLineIds.casing, - mainLineIds.main, - ]) - R.orderedIds([ - arrowIds.arrowStroke, - arrowIds.arrow, - arrowIds.arrowSymbolCasing, - arrowIds.arrowSymbol, - ]) - R.orderedIds([ - alternative_0_ids.restrictedArea, - alternative_1_ids.restrictedArea, - mainLineIds.restrictedArea, - ]) - /// To show on top of arrows - R.hasPrefix("poi") - R.orderedIds([ - voiceInstructionIds.layer, - voiceInstructionIds.circleLayer, - ]) - } - // Setting the top position on the map. We cannot explicitly set `.top` position because `.top` - // renders behind Place and Transit labels - SlottedRules(nil) { - R.orderedIds([ - intersectionIds.layer, - routeAlertIds.layer, - waypointIds.innerCircle, - waypointIds.markerIcon, - NavigationMapView.LayerIdentifier.puck2DLayer, - NavigationMapView.LayerIdentifier.puck3DLayer, - ]) - } - }, - legacyPosition: legacyPosition - ) - } - - private static func legacyLayerPosition( - for layerIdentifier: String, - mapView: MapView, - customRouteLineLayerPosition: MapboxMaps.LayerPosition? - ) -> MapboxMaps.LayerPosition? { - let mainLineIds = FeatureIds.RouteLine.main - if layerIdentifier.hasPrefix(mainLineIds.main), - let customRouteLineLayerPosition, - !mapView.mapboxMap.allLayerIdentifiers.contains(where: { $0.id.hasPrefix(mainLineIds.main) }) - { - return customRouteLineLayerPosition - } - - let alternative_0_ids = FeatureIds.RouteLine.alternative(idx: 0) - let alternative_1_ids = FeatureIds.RouteLine.alternative(idx: 1) - let arrowIds = FeatureIds.ManeuverArrow.nextArrow() - let waypointIds = FeatureIds.RouteWaypoints.default - let voiceInstructionIds = FeatureIds.VoiceInstruction.currentRoute - let intersectionIds = FeatureIds.IntersectionAnnotation.currentRoute - let routeAlertIds = FeatureIds.RouteAlertAnnotation.default - - let lowermostSymbolLayers: [String] = [ - alternative_0_ids.casing, - alternative_0_ids.main, - alternative_1_ids.casing, - alternative_1_ids.main, - mainLineIds.traversedRoute, - mainLineIds.casing, - mainLineIds.main, - mainLineIds.restrictedArea, - ].compactMap { $0 } - let aboveRoadLayers: [String] = [ - arrowIds.arrowStroke, - arrowIds.arrow, - arrowIds.arrowSymbolCasing, - arrowIds.arrowSymbol, - intersectionIds.layer, - routeAlertIds.layer, - waypointIds.innerCircle, - waypointIds.markerIcon, - ] - let uppermostSymbolLayers: [String] = [ - voiceInstructionIds.layer, - voiceInstructionIds.circleLayer, - NavigationMapView.LayerIdentifier.puck2DLayer, - NavigationMapView.LayerIdentifier.puck3DLayer, - ] - let isLowermostLayer = lowermostSymbolLayers.contains(layerIdentifier) - let isAboveRoadLayer = aboveRoadLayers.contains(layerIdentifier) - let allAddedLayers: [String] = lowermostSymbolLayers + aboveRoadLayers + uppermostSymbolLayers - - var layerPosition: MapboxMaps.LayerPosition? - var lowerLayers = Set() - var upperLayers = Set() - var targetLayer: String? - - if let index = allAddedLayers.firstIndex(of: layerIdentifier) { - lowerLayers = Set(allAddedLayers.prefix(upTo: index)) - if allAddedLayers.indices.contains(index + 1) { - upperLayers = Set(allAddedLayers.suffix(from: index + 1)) - } - } - - var foundAboveLayer = false - for layerInfo in mapView.mapboxMap.allLayerIdentifiers.reversed() { - if lowerLayers.contains(layerInfo.id) { - // find the topmost layer that should be below the layerIdentifier. - if !foundAboveLayer { - layerPosition = .above(layerInfo.id) - foundAboveLayer = true - } - } else if upperLayers.contains(layerInfo.id) { - // find the bottommost layer that should be above the layerIdentifier. - layerPosition = .below(layerInfo.id) - } else if isLowermostLayer { - // find the topmost non symbol layer for layerIdentifier in lowermostSymbolLayers. - if targetLayer == nil, - layerInfo.type.rawValue != "symbol", - let sourceLayer = mapView.mapboxMap.layerProperty(for: layerInfo.id, property: "source-layer") - .value as? String, - !sourceLayer.isEmpty - { - if layerInfo.type.rawValue == "circle", - let isPersistentCircle = try? mapView.mapboxMap.isPersistentLayer(id: layerInfo.id) - { - let pitchAlignment = mapView.mapboxMap.layerProperty( - for: layerInfo.id, - property: "circle-pitch-alignment" - ).value as? String - if isPersistentCircle || (pitchAlignment != "map") { - continue - } - } - targetLayer = layerInfo.id - } - } else if isAboveRoadLayer { - // find the topmost road name label layer for layerIdentifier in arrowLayers. - if targetLayer == nil, - layerInfo.id.contains("road-label"), - mapView.mapboxMap.layerExists(withId: layerInfo.id) - { - targetLayer = layerInfo.id - } - } else { - // find the topmost layer for layerIdentifier in uppermostSymbolLayers. - if targetLayer == nil, - let sourceLayer = mapView.mapboxMap.layerProperty(for: layerInfo.id, property: "source-layer") - .value as? String, - !sourceLayer.isEmpty - { - targetLayer = layerInfo.id - } - } - } - - guard let targetLayer else { return layerPosition } - guard let layerPosition else { return .above(targetLayer) } - - if isLowermostLayer { - // For layers should be below symbol layers. - if case .below(let sequenceLayer) = layerPosition, !lowermostSymbolLayers.contains(sequenceLayer) { - // If the sequenceLayer isn't in lowermostSymbolLayers, it's above symbol layer. - // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost non symbol - // layer, - // but under the symbol layers. - return .above(targetLayer) - } - } else if isAboveRoadLayer { - // For layers should be above road name labels but below other symbol layers. - if case .below(let sequenceLayer) = layerPosition, uppermostSymbolLayers.contains(sequenceLayer) { - // If the sequenceLayer is in uppermostSymbolLayers, it's above all symbol layers. - // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost road name - // symbol layer. - return .above(targetLayer) - } else if case .above(let sequenceLayer) = layerPosition, lowermostSymbolLayers.contains(sequenceLayer) { - // If the sequenceLayer is in lowermostSymbolLayers, it's below all symbol layers. - // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost road name - // symbol layer. - return .above(targetLayer) - } - } else { - // For other layers should be uppermost and above symbol layers. - if case .above(let sequenceLayer) = layerPosition, !uppermostSymbolLayers.contains(sequenceLayer) { - // If the sequenceLayer isn't in uppermostSymbolLayers, it's below some symbol layers. - // So for the layerIdentifier, it should be put above the targetLayer, which is the topmost layer. - return .above(targetLayer) - } - } - - return layerPosition - } -} - -extension NavigationMapStyleManager { - // TODO: These ids are specific to Standard style, we should allow customers to customize this - var poiLayerIds: [String] { - let poiLayerIds = layerIds.filter { layerId in - NavigationMapView.LayerIdentifier.clickablePoiLabels.contains { - layerId.hasPrefix($0) - } - } - return Array(poiLayerIds) - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift deleted file mode 100644 index e4566f14a..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/RouteAlertsAnnotationsMapFeatures.swift +++ /dev/null @@ -1,373 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxDirections -import MapboxMaps -import MapboxNavigationNative -import enum SwiftUI.ColorScheme -import UIKit -import Foundation - -extension NavigationRoutes { - func routeAlertsAnnotationsMapFeatures( - ids: FeatureIds.RouteAlertAnnotation, - distanceTraveled: CLLocationDistance, - customizedLayerProvider: CustomizedLayerProvider, - excludedRouteAlertTypes: RoadAlertType - ) -> [MapFeature] { - let convertedRouteAlerts = mainRoute.nativeRoute.getRouteInfo().alerts.map { - RoadObjectAhead( - roadObject: RoadObject($0.roadObject), - distance: $0.distanceToStart - ) - } - - return convertedRouteAlerts.routeAlertsAnnotationsMapFeatures( - ids: ids, - distanceTraveled: distanceTraveled, - customizedLayerProvider: customizedLayerProvider, - excludedRouteAlertTypes: excludedRouteAlertTypes - ) - } -} - -extension [RoadObjectAhead] { - func routeAlertsAnnotationsMapFeatures( - ids: FeatureIds.RouteAlertAnnotation, - distanceTraveled: CLLocationDistance, - customizedLayerProvider: CustomizedLayerProvider, - excludedRouteAlertTypes: RoadAlertType - ) -> [MapFeature] { - let featureCollection = FeatureCollection(features: roadObjectsFeatures( - for: self, - currentDistance: distanceTraveled, - excludedRouteAlertTypes: excludedRouteAlertTypes - )) - let layers: [any Layer] = [ - with(SymbolLayer(id: ids.layer, source: ids.source)) { - $0.iconImage = .expression(Exp(.get) { RoadObjectInfo.objectImageType }) - $0.minZoom = 10 - - $0.iconSize = .expression( - Exp(.interpolate) { - Exp(.linear) - Exp(.zoom) - Self.interpolationFactors.mapValues { $0 * 0.2 } - } - ) - - $0.iconColor = .expression(Exp(.get) { RoadObjectInfo.objectColor }) - }, - ] - return [ - GeoJsonMapFeature( - id: ids.featureId, - sources: [ - .init( - id: ids.source, - geoJson: .featureCollection(featureCollection) - ), - ], - customizeSource: { _, _ in }, - layers: layers.map { customizedLayerProvider.customizedLayer($0) }, - onBeforeAdd: { mapView in - Self.upsertRouteAlertsSymbolImages( - map: mapView.mapboxMap - ) - }, - onUpdate: { mapView in - Self.upsertRouteAlertsSymbolImages( - map: mapView.mapboxMap - ) - }, - onAfterRemove: { mapView in - do { - try Self.removeRouteAlertSymbolImages( - from: mapView.mapboxMap - ) - } catch { - Log.error( - "Failed to remove route alerts annotation images with error \(error)", - category: .navigationUI - ) - } - } - ), - ] - } - - private static let interpolationFactors = [ - 10.0: 1.0, - 14.5: 3.0, - 17.0: 6.0, - 22.0: 8.0, - ] - - private func roadObjectsFeatures( - for alerts: [RoadObjectAhead], - currentDistance: CLLocationDistance, - excludedRouteAlertTypes: RoadAlertType - ) -> [Feature] { - var features = [Feature]() - for alert in alerts where !alert.isExcluded(excludedRouteAlertTypes: excludedRouteAlertTypes) { - guard alert.distance == nil || alert.distance! >= currentDistance, - let objectInfo = info(for: alert.roadObject.kind) - else { continue } - let object = alert.roadObject - func addImage( - _ coordinate: LocationCoordinate2D, - _ distance: LocationDistance?, - color: UIColor? = nil - ) { - var feature = Feature(geometry: .point(.init(coordinate))) - let identifier: FeatureIdentifier = - .string("road-alert-\(coordinate.latitude)-\(coordinate.longitude)-\(features.count)") - let colorHex = (color ?? objectInfo.color ?? UIColor.gray).hexString - let properties: [String: JSONValue?] = [ - RoadObjectInfo.objectColor: JSONValue(rawValue: colorHex ?? UIColor.gray.hexString!), - RoadObjectInfo.objectImageType: .string(objectInfo.imageType.rawValue), - RoadObjectInfo.objectDistanceFromStart: .number(distance ?? 0.0), - RoadObjectInfo.distanceTraveled: .number(0.0), - ] - feature.properties = properties - feature.identifier = identifier - features.append(feature) - } - switch object.location { - case .routeAlert(shape: .lineString(let shape)): - guard - let startCoordinate = shape.coordinates.first, - let endCoordinate = shape.coordinates.last - else { - break - } - - if alert.distance.map({ $0 > 0 }) ?? true { - addImage(startCoordinate, alert.distance, color: .blue) - } - addImage(endCoordinate, alert.distance.map { $0 + (object.length ?? 0) }, color: .red) - case .routeAlert(shape: .point(let point)): - addImage(point.coordinates, alert.distance, color: nil) - case .openLRPoint(position: _, sideOfRoad: _, orientation: _, coordinate: let coordinates): - addImage(coordinates, alert.distance, color: nil) - case .openLRLine(path: _, shape: let geometry): - guard - let shape = openLRShape(from: geometry), - let startCoordinate = shape.coordinates.first, - let endCoordinate = shape.coordinates.last - else { - break - } - if alert.distance.map({ $0 > 0 }) ?? true { - addImage(startCoordinate, alert.distance, color: .blue) - } - addImage(endCoordinate, alert.distance.map { $0 + (object.length ?? 0) }, color: .red) - case .subgraph(enters: let enters, exits: let exits, shape: _, edges: _): - for enter in enters { - addImage(enter.coordinate, nil, color: .blue) - } - for exit in exits { - addImage(exit.coordinate, nil, color: .red) - } - default: - Log.error( - "Unexpected road object as Route Alert: \(object.identifier):\(object.kind)", - category: .navigationUI - ) - } - } - return features - } - - private func openLRShape(from geometry: Geometry) -> LineString? { - switch geometry { - case .point(let point): - return .init([point.coordinates]) - case .lineString(let lineString): - return lineString - default: - break - } - return nil - } - - private func info(for objectKind: RoadObject.Kind) -> RoadObjectInfo? { - switch objectKind { - case .incident(let incident): - let text = incident?.description - let color = incident?.impact.map(color(for:)) - switch incident?.kind { - case .congestion: - return .init(.congestion, text: text, color: color) - case .construction: - return .init(.construction, text: text, color: color) - case .roadClosure: - return .init(.roadClosure, text: text, color: color) - case .accident: - return .init(.accident, text: text, color: color) - case .disabledVehicle: - return .init(.disabledVehicle, text: text, color: color) - case .laneRestriction: - return .init(.laneRestriction, text: text, color: color) - case .massTransit: - return .init(.massTransit, text: text, color: color) - case .miscellaneous: - return .init(.miscellaneous, text: text, color: color) - case .otherNews: - return .init(.otherNews, text: text, color: color) - case .plannedEvent: - return .init(.plannedEvent, text: text, color: color) - case .roadHazard: - return .init(.roadHazard, text: text, color: color) - case .weather: - return .init(.weather, text: text, color: color) - case .undefined, .none: - return nil - } - default: - // We only show incidents on the map - return nil - } - } - - private func color(for impact: Incident.Impact) -> UIColor { - switch impact { - case .critical: - return .red - case .major: - return .purple - case .minor: - return .orange - case .low: - return .blue - case .unknown: - return .gray - } - } - - public static func resourceBundle() -> Bundle? { - let bundle = Bundle(for: MapboxNavigationProvider.self) - if let resourceBundleURL = bundle.url(forResource: "MapboxNavigationCoreResources", withExtension: "bundle") { - return Bundle(url: resourceBundleURL) - } - return nil - } - - private static func upsertRouteAlertsSymbolImages( - map: MapboxMap - ) { - for (imageName, imageIdentifier) in imageNameToMapIdentifier(ids: RoadObjectFeature.ImageType.allCases) { - if let image = resourceBundle()?.image(named: imageName) { - map.provisionImage(id: imageIdentifier) { _ in - try map.addImage(image, id: imageIdentifier) - } - } else { - assertionFailure("No image for route alert \(imageName) in the bundle.") - } - } - } - - private static func removeRouteAlertSymbolImages( - from map: MapboxMap - ) throws { - for (_, imageIdentifier) in imageNameToMapIdentifier(ids: RoadObjectFeature.ImageType.allCases) { - try map.removeImage(withId: imageIdentifier) - } - } - - private static func imageNameToMapIdentifier( - ids: [RoadObjectFeature.ImageType] - ) -> [String: String] { - return ids.reduce(into: [String: String]()) { partialResult, type in - partialResult[type.imageName] = type.rawValue - } - } - - private struct RoadObjectFeature: Equatable { - enum ImageType: String, CaseIterable { - case accident - case congestion - case construction - case disabledVehicle = "disabled_vehicle" - case laneRestriction = "lane_restriction" - case massTransit = "mass_transit" - case miscellaneous - case otherNews = "other_news" - case plannedEvent = "planned_event" - case roadClosure = "road_closure" - case roadHazard = "road_hazard" - case weather - - var imageName: String { - switch self { - case .accident: - return "ra_accident" - case .congestion: - return "ra_congestion" - case .construction: - return "ra_construction" - case .disabledVehicle: - return "ra_disabled_vehicle" - case .laneRestriction: - return "ra_lane_restriction" - case .massTransit: - return "ra_mass_transit" - case .miscellaneous: - return "ra_miscellaneous" - case .otherNews: - return "ra_other_news" - case .plannedEvent: - return "ra_planned_event" - case .roadClosure: - return "ra_road_closure" - case .roadHazard: - return "ra_road_hazard" - case .weather: - return "ra_weather" - } - } - } - - struct Image: Equatable { - var id: String? - var type: ImageType - var coordinate: LocationCoordinate2D - var color: UIColor? - var text: String? - var isOnMainRoute: Bool - } - - struct Shape: Equatable { - var geometry: Geometry - } - - var id: String - var images: [Image] - var shape: Shape? - } - - private struct RoadObjectInfo { - var imageType: RoadObjectFeature.ImageType - var text: String? - var color: UIColor? - - init(_ imageType: RoadObjectFeature.ImageType, text: String? = nil, color: UIColor? = nil) { - self.imageType = imageType - self.text = text - self.color = color - } - - static let objectColor = "objectColor" - static let objectImageType = "objectImageType" - static let objectDistanceFromStart = "objectDistanceFromStart" - static let distanceTraveled = "distanceTraveled" - } -} - -extension RoadObjectAhead { - fileprivate func isExcluded(excludedRouteAlertTypes: RoadAlertType) -> Bool { - guard let roadAlertType = RoadAlertType(roadObjectKind: roadObject.kind) else { - return false - } - - return excludedRouteAlertTypes.contains(roadAlertType) - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift deleted file mode 100644 index 90c7a55bd..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/RouteAnnotationMapFeatures.swift +++ /dev/null @@ -1,55 +0,0 @@ -import _MapboxNavigationHelpers -import CoreLocation -import MapboxDirections -import MapboxMaps -import Turf -import UIKit - -/// Describes the possible annotation types on the route line. -public enum RouteAnnotationKind { - /// Shows the route duration. - case routeDurations - /// Shows the relative diff between the main route and the alternative. - /// The annotation is displayed in the approximate middle of the alternative steps. - case relativeDurationsOnAlternative - /// Shows the relative diff between the main route and the alternative. - /// The annotation is displayed next to the first different maneuver of the alternative road. - case relativeDurationsOnAlternativeManuever -} - -extension NavigationRoutes { - func routeDurationMapFeatures( - annotationKinds: Set, - config: MapStyleConfig - ) -> [any MapFeature] { - var showMainRoute = false - var showAlternatives = false - var showAsRelative = false - var annotateManeuver = false - for annotationKind in annotationKinds { - switch annotationKind { - case .routeDurations: - showMainRoute = true - showAlternatives = config.showsAlternatives - case .relativeDurationsOnAlternative: - showAsRelative = true - showAlternatives = config.showsAlternatives - case .relativeDurationsOnAlternativeManuever: - showAsRelative = true - annotateManeuver = true - showAlternatives = config.showsAlternatives - } - } - - return [ - ETAViewsAnnotationFeature( - for: self, - showMainRoute: showMainRoute, - showAlternatives: showAlternatives, - isRelative: showAsRelative, - annotateAtManeuver: annotateManeuver, - mapStyleConfig: config - ), - ] - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift deleted file mode 100644 index 66215f6f4..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/RouteLineMapFeatures.swift +++ /dev/null @@ -1,406 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxDirections -@_spi(Experimental) import MapboxMaps -import Turf -import UIKit - -struct LineGradientSettings { - let isSoft: Bool - let baseColor: UIColor - let featureColor: (Turf.Feature) -> UIColor -} - -struct RouteLineFeatureProvider { - var customRouteLineLayer: (String, String) -> Layer? - var customRouteCasingLineLayer: (String, String) -> Layer? - var customRouteRestrictedAreasLineLayer: (String, String) -> Layer? -} - -extension Route { - func routeLineMapFeatures( - ids: FeatureIds.RouteLine, - offset: Double, - isSoftGradient: Bool, - isAlternative: Bool, - config: MapStyleConfig, - featureProvider: RouteLineFeatureProvider, - customizedLayerProvider: CustomizedLayerProvider - ) -> [any MapFeature] { - var features: [any MapFeature] = [] - - if let shape { - let congestionFeatures = congestionFeatures( - legIndex: nil, - rangesConfiguration: config.congestionConfiguration.ranges - ) - let gradientStops = routeLineCongestionGradient( - congestionFeatures: congestionFeatures, - isMain: !isAlternative, - isSoft: isSoftGradient, - config: config - ) - let colors = config.congestionConfiguration.colors - let trafficGradient: Value = .expression( - .routeLineGradientExpression( - gradientStops, - lineBaseColor: isAlternative ? colors.alternativeRouteColors.unknown : colors.mainRouteColors - .unknown, - isSoft: isSoftGradient - ) - ) - - var sources: [GeoJsonMapFeature.Source] = [ - .init( - id: ids.source, - geoJson: .init(Feature(geometry: .lineString(shape))) - ), - ] - - let customRouteLineLayer = featureProvider.customRouteLineLayer(ids.main, ids.source) - let customRouteCasingLineLayer = featureProvider.customRouteCasingLineLayer(ids.casing, ids.source) - var layers: [any Layer] = [ - customRouteLineLayer ?? customizedLayerProvider.customizedLayer(defaultRouteLineLayer( - ids: ids, - isAlternative: isAlternative, - trafficGradient: trafficGradient, - config: config - )), - customRouteCasingLineLayer ?? customizedLayerProvider.customizedLayer(defaultRouteCasingLineLayer( - ids: ids, - isAlternative: isAlternative, - config: config - )), - ] - - if let traversedRouteColor = config.traversedRouteColor, !isAlternative, config.routeLineTracksTraversal { - layers.append( - customizedLayerProvider.customizedLayer(defaultTraversedRouteLineLayer( - ids: ids, - traversedRouteColor: traversedRouteColor, - config: config - )) - ) - } - - let restrictedRoadsFeatures: [Feature]? = config.isRestrictedAreaEnabled ? restrictedRoadsFeatures() : nil - let restrictedAreaGradientExpression: Value? = restrictedRoadsFeatures - .map { routeLineRestrictionsGradient($0, config: config) } - .map { - .expression( - MapboxMaps.Expression.routeLineGradientExpression( - $0, - lineBaseColor: config.routeRestrictedAreaColor - ) - ) - } - - if let restrictedRoadsFeatures, let restrictedAreaGradientExpression { - let shape = LineString(restrictedRoadsFeatures.compactMap { - guard case .lineString(let lineString) = $0.geometry else { - return nil - } - return lineString.coordinates - }.reduce([CLLocationCoordinate2D](), +)) - - sources.append( - .init( - id: ids.restrictedAreaSource, - geoJson: .geometry(.lineString(shape)) - ) - ) - let customRouteRestrictedAreasLine = featureProvider.customRouteRestrictedAreasLineLayer( - ids.restrictedArea, - ids.restrictedAreaSource - ) - - layers.append( - customRouteRestrictedAreasLine ?? - customizedLayerProvider.customizedLayer(defaultRouteRestrictedAreasLine( - ids: ids, - gradientExpression: restrictedAreaGradientExpression, - config: config - )) - ) - } - - features.append( - GeoJsonMapFeature( - id: ids.main, - sources: sources, - customizeSource: { source, _ in - source.lineMetrics = true - source.tolerance = 0.375 - }, - layers: layers, - onAfterAdd: { mapView in - mapView.mapboxMap.setRouteLineOffset(offset, for: ids) - }, - onUpdate: { mapView in - mapView.mapboxMap.setRouteLineOffset(offset, for: ids) - }, - onAfterUpdate: { mapView in - let map: MapboxMap = mapView.mapboxMap - try map.updateLayer(withId: ids.main, type: LineLayer.self, update: { layer in - layer.lineGradient = trafficGradient - }) - if let restrictedAreaGradientExpression { - try map.updateLayer(withId: ids.restrictedArea, type: LineLayer.self, update: { layer in - layer.lineGradient = restrictedAreaGradientExpression - }) - } - } - ) - ) - } - - return features - } - - private func defaultRouteLineLayer( - ids: FeatureIds.RouteLine, - isAlternative: Bool, - trafficGradient: Value, - config: MapStyleConfig - ) -> LineLayer { - let colors = config.congestionConfiguration.colors - let routeColors = isAlternative ? colors.alternativeRouteColors : colors.mainRouteColors - return with(LineLayer(id: ids.main, source: ids.source)) { - $0.lineColor = .constant(.init(routeColors.unknown)) - $0.lineWidth = .expression(.routeLineWidthExpression()) - $0.lineJoin = .constant(.round) - $0.lineCap = .constant(.round) - $0.lineGradient = trafficGradient - $0.lineDepthOcclusionFactor = config.occlusionFactor - $0.lineEmissiveStrength = .constant(1) - } - } - - private func defaultRouteCasingLineLayer( - ids: FeatureIds.RouteLine, - isAlternative: Bool, - config: MapStyleConfig - ) -> LineLayer { - let lineColor = isAlternative ? config.routeAlternateCasingColor : config.routeCasingColor - return with(LineLayer(id: ids.casing, source: ids.source)) { - $0.lineColor = .constant(.init(lineColor)) - $0.lineWidth = .expression(.routeCasingLineWidthExpression()) - $0.lineJoin = .constant(.round) - $0.lineCap = .constant(.round) - $0.lineDepthOcclusionFactor = config.occlusionFactor - $0.lineEmissiveStrength = .constant(1) - } - } - - private func defaultTraversedRouteLineLayer( - ids: FeatureIds.RouteLine, - traversedRouteColor: UIColor, - config: MapStyleConfig - ) -> LineLayer { - return with(LineLayer(id: ids.traversedRoute, source: ids.source)) { - $0.lineColor = .constant(.init(traversedRouteColor)) - $0.lineWidth = .expression(.routeLineWidthExpression()) - $0.lineJoin = .constant(.round) - $0.lineCap = .constant(.round) - $0.lineDepthOcclusionFactor = config.occlusionFactor - $0.lineEmissiveStrength = .constant(1) - } - } - - private func defaultRouteRestrictedAreasLine( - ids: FeatureIds.RouteLine, - gradientExpression: Value?, - config: MapStyleConfig - ) -> LineLayer { - return with(LineLayer(id: ids.restrictedArea, source: ids.restrictedAreaSource)) { - $0.lineColor = .constant(.init(config.routeRestrictedAreaColor)) - $0.lineWidth = .expression(Expression.routeLineWidthExpression(0.5)) - $0.lineJoin = .constant(.round) - $0.lineCap = .constant(.round) - $0.lineOpacity = .constant(0.5) - $0.lineDepthOcclusionFactor = config.occlusionFactor - - $0.lineGradient = gradientExpression - $0.lineDasharray = .constant([0.5, 2.0]) - } - } - - func routeLineCongestionGradient( - congestionFeatures: [Turf.Feature]? = nil, - isMain: Bool = true, - isSoft: Bool, - config: MapStyleConfig - ) -> [Double: UIColor] { - // If `congestionFeatures` is set to nil - check if overridden route line casing is used. - let colors = config.congestionConfiguration.colors - let baseColor: UIColor = if let _ = congestionFeatures { - isMain ? colors.mainRouteColors.unknown : colors.alternativeRouteColors.unknown - } else { - config.routeCasingColor - } - let configuration = config.congestionConfiguration.colors - - let lineSettings = LineGradientSettings( - isSoft: isSoft, - baseColor: baseColor, - featureColor: { - guard config.showsTrafficOnRouteLine else { - return baseColor - } - if case .boolean(let isCurrentLeg) = $0.properties?[CurrentLegAttribute], isCurrentLeg { - let colors = isMain ? configuration.mainRouteColors : configuration.alternativeRouteColors - if case .string(let congestionLevel) = $0.properties?[CongestionAttribute] { - return congestionColor(for: congestionLevel, with: colors) - } else { - return congestionColor(for: nil, with: colors) - } - } - - return config.routeCasingColor - } - ) - - return routeLineFeaturesGradient(congestionFeatures, lineSettings: lineSettings) - } - - /// Given a congestion level, return its associated color. - func congestionColor(for congestionLevel: String?, with colors: CongestionColorsConfiguration.Colors) -> UIColor { - switch congestionLevel { - case "low": - return colors.low - case "moderate": - return colors.moderate - case "heavy": - return colors.heavy - case "severe": - return colors.severe - default: - return colors.unknown - } - } - - func routeLineFeaturesGradient( - _ routeLineFeatures: [Turf.Feature]? = nil, - lineSettings: LineGradientSettings - ) -> [Double: UIColor] { - var gradientStops = [Double: UIColor]() - var distanceTraveled = 0.0 - - if let routeLineFeatures { - let routeDistance = routeLineFeatures.compactMap { feature -> LocationDistance? in - if case .lineString(let lineString) = feature.geometry { - return lineString.distance() - } else { - return nil - } - }.reduce(0, +) - // lastRecordSegment records the last segmentEndPercentTraveled and associated congestion color added to the - // gradientStops. - var lastRecordSegment: (Double, UIColor) = (0.0, .clear) - - for (index, feature) in routeLineFeatures.enumerated() { - let associatedFeatureColor = lineSettings.featureColor(feature) - - guard case .lineString(let lineString) = feature.geometry, - let distance = lineString.distance() - else { - if gradientStops.isEmpty { - gradientStops[0.0] = lineSettings.baseColor - } - return gradientStops - } - let minimumPercentGap = 2e-16 - let stopGap = (routeDistance > 0.0) ? max( - min(GradientCongestionFadingDistance, distance * 0.1) / routeDistance, - minimumPercentGap - ) : minimumPercentGap - - if index == routeLineFeatures.startIndex { - distanceTraveled = distanceTraveled + distance - gradientStops[0.0] = associatedFeatureColor - - if index + 1 < routeLineFeatures.count { - let segmentEndPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 - var currentGradientStop = lineSettings - .isSoft ? segmentEndPercentTraveled - stopGap : - Double(CGFloat(segmentEndPercentTraveled).nextDown) - currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) - gradientStops[currentGradientStop] = associatedFeatureColor - lastRecordSegment = (currentGradientStop, associatedFeatureColor) - } - - continue - } - - if index == routeLineFeatures.endIndex - 1 { - if associatedFeatureColor == lastRecordSegment.1 { - gradientStops[lastRecordSegment.0] = nil - } else { - let segmentStartPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 - var currentGradientStop = lineSettings - .isSoft ? segmentStartPercentTraveled + stopGap : - Double(CGFloat(segmentStartPercentTraveled).nextUp) - currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) - gradientStops[currentGradientStop] = associatedFeatureColor - } - - continue - } - - if associatedFeatureColor == lastRecordSegment.1 { - gradientStops[lastRecordSegment.0] = nil - } else { - let segmentStartPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 - var currentGradientStop = lineSettings - .isSoft ? segmentStartPercentTraveled + stopGap : - Double(CGFloat(segmentStartPercentTraveled).nextUp) - currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) - gradientStops[currentGradientStop] = associatedFeatureColor - } - - distanceTraveled = distanceTraveled + distance - let segmentEndPercentTraveled = (routeDistance > 0.0) ? distanceTraveled / routeDistance : 0 - var currentGradientStop = lineSettings - .isSoft ? segmentEndPercentTraveled - stopGap : Double(CGFloat(segmentEndPercentTraveled).nextDown) - currentGradientStop = min(max(currentGradientStop, 0.0), 1.0) - gradientStops[currentGradientStop] = associatedFeatureColor - lastRecordSegment = (currentGradientStop, associatedFeatureColor) - } - - if gradientStops.isEmpty { - gradientStops[0.0] = lineSettings.baseColor - } - - } else { - gradientStops[0.0] = lineSettings.baseColor - } - - return gradientStops - } - - func routeLineRestrictionsGradient( - _ restrictionFeatures: [Turf.Feature], - config: MapStyleConfig - ) -> [Double: UIColor] { - // If there's no restricted feature, hide the restricted route line layer. - guard restrictionFeatures.count > 0 else { - let gradientStops: [Double: UIColor] = [0.0: .clear] - return gradientStops - } - - let lineSettings = LineGradientSettings( - isSoft: false, - baseColor: config.routeRestrictedAreaColor, - featureColor: { - if case .boolean(let isRestricted) = $0.properties?[RestrictedRoadClassAttribute], - isRestricted - { - return config.routeRestrictedAreaColor - } - - return .clear // forcing hiding non-restricted areas - } - ) - - return routeLineFeaturesGradient(restrictionFeatures, lineSettings: lineSettings) - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift b/ios/Classes/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift deleted file mode 100644 index cda245e09..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/VoiceInstructionsMapFeatures.swift +++ /dev/null @@ -1,67 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxDirections -import MapboxMaps -import Turf -import MapboxNavigationCore - -extension Route { - func voiceInstructionMapFeatures( - ids: FeatureIds.VoiceInstruction, - customizedLayerProvider: CustomizedLayerProvider - ) -> [any MapFeature] { - var featureCollection = FeatureCollection(features: []) - - for (legIndex, leg) in legs.enumerated() { - for (stepIndex, step) in leg.steps.enumerated() { - guard let instructions = step.instructionsSpokenAlongStep else { continue } - for instruction in instructions { - guard let shape = legs[legIndex].steps[stepIndex].shape, - let coordinateFromStart = LineString(shape.coordinates.reversed()) - .coordinateFromStart(distance: instruction.distanceAlongStep) else { continue } - - var feature = Feature(geometry: .point(Point(coordinateFromStart))) - feature.properties = [ - "instruction": .string(instruction.text), - ] - featureCollection.features.append(feature) - } - } - } - - let layers: [any Layer] = [ - with(SymbolLayer(id: ids.layer, source: ids.source)) { - let instruction = Exp(.toString) { - Exp(.get) { - "instruction" - } - } - - $0.textField = .expression(instruction) - $0.textSize = .constant(14) - $0.textHaloWidth = .constant(1) - $0.textHaloColor = .constant(.init(.white)) - $0.textOpacity = .constant(0.75) - $0.textAnchor = .constant(.bottom) - $0.textJustify = .constant(.left) - }, - with(CircleLayer(id: ids.circleLayer, source: ids.source)) { - $0.circleRadius = .constant(5) - $0.circleOpacity = .constant(0.75) - $0.circleColor = .constant(.init(.white)) - }, - ] - return [ - GeoJsonMapFeature( - id: ids.source, - sources: [ - .init( - id: ids.source, - geoJson: .featureCollection(featureCollection) - ), - ], - customizeSource: { _, _ in }, - layers: layers.map { customizedLayerProvider.customizedLayer($0) } - ), - ] - } -} diff --git a/ios/Classes/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift b/ios/Classes/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift deleted file mode 100644 index 673da318a..000000000 --- a/ios/Classes/MapboxNavigationCore/Map/Style/WaypointsMapFeature.swift +++ /dev/null @@ -1,157 +0,0 @@ -import _MapboxNavigationHelpers -import MapboxDirections -import MapboxMaps -import Turf -import UIKit -import MapboxNavigationCore - -struct WaypointFeatureProvider { - var customFeatures: ([Waypoint], Int) -> FeatureCollection? - var customCirleLayer: (String, String) -> CircleLayer? - var customSymbolLayer: (String, String) -> SymbolLayer? -} - -@MainActor -extension Route { - /// Generates a map feature that visually represents waypoints along a route line. - /// The waypoints include the start, destination, and any intermediate waypoints. - /// - Important: Only intermediate waypoints are marked with pins. The starting point and destination are excluded - /// from this. - func waypointsMapFeature( - mapView: MapView, - legIndex: Int, - config: MapStyleConfig, - featureProvider: WaypointFeatureProvider, - customizedLayerProvider: CustomizedLayerProvider - ) -> MapFeature? { - guard let startWaypoint = legs.first?.source else { return nil } - guard let destinationWaypoint = legs.last?.destination else { return nil } - - let intermediateWaypoints = config.showsIntermediateWaypoints - ? legs.dropLast().compactMap(\.destination) - : [] - let waypoints = [startWaypoint] + intermediateWaypoints + [destinationWaypoint] - - registerIntermediateWaypointImage(in: mapView) - - let customFeatures = featureProvider.customFeatures(waypoints, legIndex) - - return waypointsMapFeature( - with: customFeatures ?? waypointsFeatures(legIndex: legIndex, waypoints: waypoints), - config: config, - featureProvider: featureProvider, - customizedLayerProvider: customizedLayerProvider - ) - } - - private func waypointsFeatures(legIndex: Int, waypoints: [Waypoint]) -> FeatureCollection { - FeatureCollection( - features: waypoints.enumerated().map { waypointIndex, waypoint in - var feature = Feature(geometry: .point(Point(waypoint.coordinate))) - var properties: [String: JSONValue] = [:] - properties["waypointCompleted"] = .boolean(waypointIndex <= legIndex) - properties["waipointIconImage"] = waypointIndex > 0 && waypointIndex < waypoints.count - 1 - ? .string(NavigationMapView.ImageIdentifier.midpointMarkerImage) - : nil - feature.properties = properties - - return feature - } - ) - } - - private func registerIntermediateWaypointImage(in mapView: MapView) { - let intermediateWaypointImageId = NavigationMapView.ImageIdentifier.midpointMarkerImage - mapView.mapboxMap.provisionImage(id: intermediateWaypointImageId) { - try $0.addImage( - UIImage.midpointMarkerImage, - id: intermediateWaypointImageId, - stretchX: [], - stretchY: [] - ) - } - } - - private func waypointsMapFeature( - with features: FeatureCollection, - config: MapStyleConfig, - featureProvider: WaypointFeatureProvider, - customizedLayerProvider: CustomizedLayerProvider - ) -> MapFeature { - let circleLayer = featureProvider.customCirleLayer( - FeatureIds.RouteWaypoints.default.innerCircle, - FeatureIds.RouteWaypoints.default.source - ) ?? customizedLayerProvider.customizedLayer(defaultCircleLayer(config: config)) - - let symbolLayer = featureProvider.customSymbolLayer( - FeatureIds.RouteWaypoints.default.markerIcon, - FeatureIds.RouteWaypoints.default.source - ) ?? customizedLayerProvider.customizedLayer(defaultSymbolLayer) - - return GeoJsonMapFeature( - id: FeatureIds.RouteWaypoints.default.featureId, - sources: [ - .init( - id: FeatureIds.RouteWaypoints.default.source, - geoJson: .featureCollection(features) - ), - ], - customizeSource: { _, _ in }, - layers: [circleLayer, symbolLayer], - onBeforeAdd: { _ in }, - onAfterRemove: { _ in } - ) - } - - private func defaultCircleLayer(config: MapStyleConfig) -> CircleLayer { - with( - CircleLayer( - id: FeatureIds.RouteWaypoints.default.innerCircle, - source: FeatureIds.RouteWaypoints.default.source - ) - ) { - let opacity = Exp(.switchCase) { - Exp(.any) { - Exp(.get) { - "waypointCompleted" - } - } - 0 - 1 - } - - $0.circleColor = .constant(.init(config.waypointColor)) - $0.circleOpacity = .expression(opacity) - $0.circleEmissiveStrength = .constant(1) - $0.circleRadius = .expression(.routeCasingLineWidthExpression(0.5)) - $0.circleStrokeColor = .constant(.init(config.waypointStrokeColor)) - $0.circleStrokeWidth = .expression(.routeCasingLineWidthExpression(0.14)) - $0.circleStrokeOpacity = .expression(opacity) - $0.circlePitchAlignment = .constant(.map) - } - } - - private var defaultSymbolLayer: SymbolLayer { - with( - SymbolLayer( - id: FeatureIds.RouteWaypoints.default.markerIcon, - source: FeatureIds.RouteWaypoints.default.source - ) - ) { - let opacity = Exp(.switchCase) { - Exp(.any) { - Exp(.get) { - "waypointCompleted" - } - } - 0 - 1 - } - $0.iconOpacity = .expression(opacity) - $0.iconImage = .expression(Exp(.get) { "waipointIconImage" }) - $0.iconAnchor = .constant(.bottom) - $0.iconOffset = .constant([0, 15]) - $0.iconAllowOverlap = .constant(true) - } - } -} diff --git a/ios/Classes/NavigationController+ContinuousAlternatives.swift b/ios/Classes/NavigationController+ContinuousAlternatives.swift deleted file mode 100644 index 2896e2b1a..000000000 --- a/ios/Classes/NavigationController+ContinuousAlternatives.swift +++ /dev/null @@ -1,65 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import MapboxDirections -import Turf -import UIKit -import MapboxNavigationCore - -extension NavigationController { - /// Returns a list of the ``AlternativeRoute``s, that are close to a certain point and are within threshold distance - /// defined in ``NavigationMapView/tapGestureDistanceThreshold``. - /// - /// - parameter point: Point on the screen. - /// - returns: List of the alternative routes, which were found. If there are no continuous alternatives routes on - /// the map view `nil` will be returned. - /// An empty array is returned if no alternative route was tapped or if there are multiple equally fitting - /// routes at the tap coordinate. - func continuousAlternativeRoutes(closeTo point: CGPoint) -> [AlternativeRoute]? { - guard let routes, !routes.alternativeRoutes.isEmpty - else { - return nil - } - - // Workaround for XCode 12.5 compilation bug - typealias RouteWithMetadata = (route: Route, index: Int, distance: LocationDistance) - - let continuousAlternatives = routes.alternativeRoutes - // Add the main route to detect if the main route is the closest to the point. The main route is excluded from - // the result array. - let allRoutes = [routes.mainRoute.route] + continuousAlternatives.map { $0.route } - - // Filter routes with at least 2 coordinates and within tap distance. - let tapCoordinate = mapView.mapboxMap.coordinate(for: point) - let routeMetadata = allRoutes.enumerated() - .compactMap { index, route -> RouteWithMetadata? in - guard route.shape?.coordinates.count ?? 0 > 1 else { - return nil - } - guard let closestCoordinate = route.shape?.closestCoordinate(to: tapCoordinate)?.coordinate else { - return nil - } - - let closestPoint = mapView.mapboxMap.point(for: closestCoordinate) - guard closestPoint.distance(to: point) < tapGestureDistanceThreshold else { - return nil - } - let distance = closestCoordinate.distance(to: tapCoordinate) - return RouteWithMetadata(route: route, index: index, distance: distance) - } - - // Sort routes by closest distance to tap gesture. - let closest = routeMetadata.sorted { (lhs: RouteWithMetadata, rhs: RouteWithMetadata) -> Bool in - return lhs.distance < rhs.distance - } - - // Exclude the routes if the distance is the same and we cannot distinguish the routes. - if routeMetadata.count > 1, abs(routeMetadata[0].distance - routeMetadata[1].distance) < 1e-6 { - return [] - } - - return closest.compactMap { (item: RouteWithMetadata) -> AlternativeRoute? in - guard item.index > 0 else { return nil } - return continuousAlternatives[item.index - 1] - } - } -} diff --git a/ios/Classes/NavigationController+Gestures.swift b/ios/Classes/NavigationController+Gestures.swift deleted file mode 100644 index 5c47c5d6e..000000000 --- a/ios/Classes/NavigationController+Gestures.swift +++ /dev/null @@ -1,209 +0,0 @@ -import _MapboxNavigationHelpers -import Foundation -import MapboxDirections -import MapboxMaps -import Turf -import UIKit -import MapboxNavigationCore - -extension NavigationController { - func setupGestureRecognizers() { - // Gesture recognizer, which is used to detect long taps on any point on the map. - let longPressGestureRecognizer = UILongPressGestureRecognizer( - target: self, - action: #selector(handleLongPress(_:)) - ) - //addGestureRecognizer(longPressGestureRecognizer) - - // Gesture recognizer, which is used to detect taps on route line, waypoint or POI - mapViewTapGestureRecognizer = UITapGestureRecognizer( - target: self, - action: #selector(didReceiveTap(gesture:)) - ) - mapViewTapGestureRecognizer.delegate = self - mapView.addGestureRecognizer(mapViewTapGestureRecognizer) - - makeGestureRecognizersDisableCameraFollowing() - makeTapGestureRecognizerStopAnimatedTransitions() - } - - @objc - private func handleLongPress(_ gesture: UIGestureRecognizer) { - guard gesture.state == .began else { return } - let gestureLocation = gesture.location(in: self.mapView) - Task { @MainActor in - let point = await mapPoint(at: gestureLocation) - //delegate?.navigationMapView(self, userDidLongTap: point) - } - } - - /// Modifies `MapView` gesture recognizers to disable follow mode and move `NavigationCamera` to - /// `NavigationCameraState.idle` state. - private func makeGestureRecognizersDisableCameraFollowing() { - for gestureRecognizer in mapView.gestureRecognizers ?? [] - where gestureRecognizer is UIPanGestureRecognizer - || gestureRecognizer is UIRotationGestureRecognizer - || gestureRecognizer is UIPinchGestureRecognizer - || gestureRecognizer == mapView.gestures.doubleTapToZoomInGestureRecognizer - || gestureRecognizer == mapView.gestures.doubleTouchToZoomOutGestureRecognizer - - { - gestureRecognizer.addTarget(self, action: #selector(switchToIdleCamera)) - } - } - - private func makeTapGestureRecognizerStopAnimatedTransitions() { - for gestureRecognizer in mapView.gestureRecognizers ?? [] - where gestureRecognizer is UITapGestureRecognizer - && gestureRecognizer != mapView.gestures.doubleTouchToZoomOutGestureRecognizer - { - gestureRecognizer.addTarget(self, action: #selector(switchToIdleCameraIfNotFollowing)) - } - } - - @objc - private func switchToIdleCamera() { - update(navigationCameraState: .idle) - } - - @objc - private func switchToIdleCameraIfNotFollowing() { - guard navigationCamera.currentCameraState != .following else { return } - update(navigationCameraState: .idle) - } - - /// Fired when NavigationMapView detects a tap not handled elsewhere by other gesture recognizers. - @objc - private func didReceiveTap(gesture: UITapGestureRecognizer) { - guard gesture.state == .recognized else { return } - let tapPoint = gesture.location(in: mapView) - - Task { - if let allRoutes = routes?.allRoutes() { - let waypointTest = legSeparatingWaypoints(on: allRoutes, closeTo: tapPoint) - if let selected = waypointTest?.first { - //delegate?.navigationMapView(self, didSelect: selected) - return - } - } - - if let alternativeRoute = continuousAlternativeRoutes(closeTo: tapPoint)?.first { - //delegate?.navigationMapView(self, didSelect: alternativeRoute) - return - } - - let point = await mapPoint(at: tapPoint) - - if point.name != nil { - //delegate?.navigationMapView(self, userDidTap: point) - } - } - } - - func legSeparatingWaypoints(on routes: [Route], closeTo point: CGPoint) -> [Waypoint]? { - // In case if route does not contain more than one leg - do nothing. - let multipointRoutes = routes.filter { $0.legs.count > 1 } - guard multipointRoutes.count > 0 else { return nil } - - let waypoints = multipointRoutes.compactMap { route in - route.legs.dropLast().compactMap { $0.destination } - }.flatMap { $0 } - - // Sort the array in order of closest to tap. - let tapCoordinate = mapView.mapboxMap.coordinate(for: point) - let closest = waypoints.sorted { left, right -> Bool in - let leftDistance = left.coordinate.projectedDistance(to: tapCoordinate) - let rightDistance = right.coordinate.projectedDistance(to: tapCoordinate) - return leftDistance < rightDistance - } - - // Filter to see which ones are under threshold. - let candidates = closest.filter { - let coordinatePoint = mapView.mapboxMap.point(for: $0.coordinate) - - return coordinatePoint.distance(to: point) < tapGestureDistanceThreshold - } - - return candidates - } - - private func mapPoint(at point: CGPoint) async -> MapPoint { - let options = MapboxMaps.RenderedQueryOptions(layerIds: mapStyleManager.poiLayerIds, filter: nil) - let rectSize = poiClickableAreaSize - let rect = CGRect(x: point.x - rectSize / 2, y: point.y - rectSize / 2, width: rectSize, height: rectSize) - -// let features = try? await mapView.mapboxMap.queryRenderedFeatures(with: rect, options: options) -// if let feature = features?.first?.queriedFeature.feature, -// case .string(let poiName) = feature[property: .poiName, languageCode: nil], -// case .point(let point) = feature.geometry -// { -// return MapPoint(name: poiName, coordinate: point.coordinates) -// } else { -// let coordinate = mapView.mapboxMap.coordinate(for: point) -// return MapPoint(name: nil, coordinate: coordinate) -// } - let coordinate = mapView.mapboxMap.coordinate(for: point) - return MapPoint(name: nil, coordinate: coordinate) - } -} - -// MARK: - GestureManagerDelegate - -extension NavigationController: GestureManagerDelegate { - public nonisolated func gestureManager( - _ gestureManager: MapboxMaps.GestureManager, - didBegin gestureType: MapboxMaps.GestureType - ) { - guard gestureType != .singleTap else { return } - - MainActor.assumingIsolated { - //delegate?.navigationMapViewUserDidStartInteraction(self) - } - } - - public nonisolated func gestureManager( - _ gestureManager: MapboxMaps.GestureManager, - didEnd gestureType: MapboxMaps.GestureType, - willAnimate: Bool - ) { - guard gestureType != .singleTap else { return } - - MainActor.assumingIsolated { - //delegate?.navigationMapViewUserDidEndInteraction(self) - } - } - - public nonisolated func gestureManager( - _ gestureManager: MapboxMaps.GestureManager, - didEndAnimatingFor gestureType: MapboxMaps.GestureType - ) {} -} - -// MARK: - UIGestureRecognizerDelegate - -extension NavigationController: UIGestureRecognizerDelegate { - public func gestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer - ) -> Bool { - if gestureRecognizer is UITapGestureRecognizer, - otherGestureRecognizer is UITapGestureRecognizer - { - return true - } - - return false - } - - public func gestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer - ) -> Bool { - if gestureRecognizer is UITapGestureRecognizer, - otherGestureRecognizer == mapView.gestures.doubleTapToZoomInGestureRecognizer - { - return true - } - return false - } -} diff --git a/ios/Classes/NavigationController+VanishingRouteLine.swift b/ios/Classes/NavigationController+VanishingRouteLine.swift deleted file mode 100644 index 0b9e74357..000000000 --- a/ios/Classes/NavigationController+VanishingRouteLine.swift +++ /dev/null @@ -1,213 +0,0 @@ -import _MapboxNavigationHelpers -import CoreLocation -import MapboxDirections -import MapboxMaps -import UIKit -import MapboxNavigationCore - -extension NavigationController { - struct RoutePoints { - var nestedList: [[[CLLocationCoordinate2D]]] - var flatList: [CLLocationCoordinate2D] - } - - struct RouteLineGranularDistances { - var distance: Double - var distanceArray: [RouteLineDistancesIndex] - } - - struct RouteLineDistancesIndex { - var point: CLLocationCoordinate2D - var distanceRemaining: Double - } - - // MARK: Customizing and Displaying the Route Line(s) - - func initPrimaryRoutePoints(route: Route) { - routePoints = parseRoutePoints(route: route) - routeLineGranularDistances = calculateGranularDistances(routePoints?.flatList ?? []) - } - - /// Transform the route data into nested arrays of legs -> steps -> coordinates. - /// The first and last point of adjacent steps overlap and are duplicated. - func parseRoutePoints(route: Route) -> RoutePoints { - let nestedList = route.legs.map { (routeLeg: RouteLeg) -> [[CLLocationCoordinate2D]] in - return routeLeg.steps.map { (routeStep: RouteStep) -> [CLLocationCoordinate2D] in - if let routeShape = routeStep.shape { - return routeShape.coordinates - } else { - return [] - } - } - } - let flatList = nestedList.flatMap { $0.flatMap { $0.compactMap { $0 } } } - return RoutePoints(nestedList: nestedList, flatList: flatList) - } - - func updateRouteLine(routeProgress: MapboxNavigationCore.RouteProgress) { - updateIntersectionAnnotations(routeProgress: routeProgress) - if let routes { - mapStyleManager.updateRouteAlertsAnnotations( - navigationRoutes: routes, - excludedRouteAlertTypes: excludedRouteAlertTypes, - distanceTraveled: routeProgress.distanceTraveled - ) - } - - if routeLineTracksTraversal, routes != nil { - guard !routeProgress.routeIsComplete else { - mapStyleManager.removeRoutes() - mapStyleManager.removeArrows() - return - } - - updateUpcomingRoutePointIndex(routeProgress: routeProgress) - } - updateArrow(routeProgress: routeProgress) - } - - func updateAlternatives(routeProgress: MapboxNavigationCore.RouteProgress?) { - guard let routes = routeProgress?.navigationRoutes ?? routes else { return } - show(routes, routeAnnotationKinds: routeAnnotationKinds) - } - - func updateIntersectionAnnotations(routeProgress: MapboxNavigationCore.RouteProgress?) { - if let routeProgress, showsIntersectionAnnotations { - mapStyleManager.updateIntersectionAnnotations(routeProgress: routeProgress) - } else { - mapStyleManager.removeIntersectionAnnotations() - } - } - - /// Find and cache the index of the upcoming [RouteLineDistancesIndex]. - func updateUpcomingRoutePointIndex(routeProgress: RouteProgress) { - guard let completeRoutePoints = routePoints, - completeRoutePoints.nestedList.indices.contains(routeProgress.legIndex) - else { - routeRemainingDistancesIndex = nil - return - } - let currentLegProgress = routeProgress.currentLegProgress - let currentStepProgress = routeProgress.currentLegProgress.currentStepProgress - let currentLegSteps = completeRoutePoints.nestedList[routeProgress.legIndex] - var allRemainingPoints = 0 - // Find the count of remaining points in the current step. - let lineString = currentStepProgress.step.shape ?? LineString([]) - // If user hasn't arrived at current step. All the coordinates will be included to the remaining points. - if currentStepProgress.distanceTraveled < 0 { - allRemainingPoints += currentLegSteps[currentLegProgress.stepIndex].count - } else if let startIndex = lineString - .indexedCoordinateFromStart(distance: currentStepProgress.distanceTraveled)?.index, - lineString.coordinates.indices.contains(startIndex) - { - allRemainingPoints += lineString.coordinates.suffix(from: startIndex + 1).dropLast().count - } - - // Add to the count of remaining points all of the remaining points on the current leg, after the current step. - if currentLegProgress.stepIndex < currentLegSteps.endIndex { - var count = 0 - for stepIndex in (currentLegProgress.stepIndex + 1).. RouteLineGranularDistances? { - if coordinates.isEmpty { return nil } - var distance = 0.0 - var indexArray = [RouteLineDistancesIndex?](repeating: nil, count: coordinates.count) - for index in stride(from: coordinates.count - 1, to: 0, by: -1) { - let curr = coordinates[index] - let prev = coordinates[index - 1] - distance += curr.projectedDistance(to: prev) - indexArray[index - 1] = RouteLineDistancesIndex(point: prev, distanceRemaining: distance) - } - indexArray[coordinates.count - 1] = RouteLineDistancesIndex( - point: coordinates[coordinates.count - 1], - distanceRemaining: 0.0 - ) - return RouteLineGranularDistances(distance: distance, distanceArray: indexArray.compactMap { $0 }) - } - - func findClosestCoordinateOnCurrentLine( - coordinate: CLLocationCoordinate2D, - granularDistances: RouteLineGranularDistances, - upcomingIndex: Int - ) -> CLLocationCoordinate2D { - guard granularDistances.distanceArray.indices.contains(upcomingIndex) else { return coordinate } - - var coordinates = [CLLocationCoordinate2D]() - - // Takes the passed 10 points and the upcoming point of route to form a sliced polyline for distance - // calculation, incase of the curved shape of route. - for index in max(0, upcomingIndex - 10)...upcomingIndex { - let point = granularDistances.distanceArray[index].point - coordinates.append(point) - } - - let polyline = LineString(coordinates) - - return polyline.closestCoordinate(to: coordinate)?.coordinate ?? coordinate - } - - /// Updates the fractionTraveled along the route line from the origin point to the indicated point. - /// - /// - parameter coordinate: Current position of the user location. - func calculateFractionTraveled(coordinate: CLLocationCoordinate2D) -> Double? { - guard let granularDistances = routeLineGranularDistances, - let index = routeRemainingDistancesIndex, - granularDistances.distanceArray.indices.contains(index) else { return nil } - let traveledIndex = granularDistances.distanceArray[index] - let upcomingPoint = traveledIndex.point - - // Project coordinate onto current line to properly find offset without an issue of back-growing route line. - let coordinate = findClosestCoordinateOnCurrentLine( - coordinate: coordinate, - granularDistances: granularDistances, - upcomingIndex: index + 1 - ) - - // Take the remaining distance from the upcoming point on the route and extends it by the exact position of the - // puck. - let remainingDistance = traveledIndex.distanceRemaining + upcomingPoint.projectedDistance(to: coordinate) - - // Calculate the percentage of the route traveled. - if granularDistances.distance > 0 { - let offset = (1.0 - remainingDistance / granularDistances.distance) - if offset >= 0 { - return offset - } else { - return nil - } - } - return nil - } - - /// Updates the route style layer and its casing style layer to gradually disappear as the user location puck - /// travels along the displayed route. - /// - /// - parameter coordinate: Current position of the user location. - func travelAlongRouteLine(to coordinate: CLLocationCoordinate2D?) { - guard let coordinate, routes != nil else { return } - if let fraction = calculateFractionTraveled(coordinate: coordinate) { - mapStyleManager.setRouteLineOffset(fraction, for: .main) - } - } -} diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index ac306a32c..078f54b3f 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -8,15 +8,7 @@ import _MapboxNavigationHelpers @MainActor final class NavigationController: NSObject, NavigationInterface { - private enum Constants { - static let initialMapRect = CGRect(x: 0, y: 0, width: 64, height: 64) - static let initialViewportPadding = UIEdgeInsets(top: 20, left: 20, bottom: 40, right: 20) - } - - let predictiveCacheManager: PredictiveCacheManager? - @Published private(set) var isInActiveNavigation: Bool = false - //@Published private(set) var currentPreviewRoutes: MapboxNavigationCore.NavigationRoutes? @Published private(set) var routes: MapboxNavigationCore.NavigationRoutes? @Published private(set) var routeProgress: MapboxNavigationCore.RouteProgress? @Published private(set) var currentLocation: CLLocation? @@ -28,488 +20,15 @@ final class NavigationController: NSObject, NavigationInterface { private let core: MapboxNavigation private var cancelables: Set = [] - private var onNavigationListener: NavigationListener? - let mapView: MapView - let navigationProvider: MapboxNavigationProvider - var navigationCamera: MapboxNavigationCore.NavigationCamera - let mapStyleManager: NavigationMapStyleManager - - /// The object that acts as the navigation delegate of the map view. - public weak var delegate: NavigationMapViewDelegate? + private var onNavigationListener: NavigationListener? - // Vanishing route line properties - var routePoints: RoutePoints? - var routeLineGranularDistances: RouteLineGranularDistances? - var routeRemainingDistancesIndex: Int? + private let navigationMapView: NavigationMapView - private var lifetimeSubscriptions: Set = [] - - /// The gesture recognizer, that is used to detect taps on waypoints and routes that are currently - /// present on the map. Enabled by default. - public internal(set) var mapViewTapGestureRecognizer: UITapGestureRecognizer! - - init(withMapView mapView: MapView, navigationProvider: MapboxNavigationProvider) { - - self.mapView = mapView - - self.navigationProvider = navigationProvider - - self.core = self.navigationProvider.mapboxNavigation - self.predictiveCacheManager = self.navigationProvider.predictiveCacheManager - - self.mapStyleManager = .init(mapView: mapView, customRouteLineLayerPosition: customRouteLineLayerPosition) - self.navigationCamera = NavigationCamera( - mapView, - location: core.navigation().locationMatching.map(\.enhancedLocation).eraseToAnyPublisher(), - routeProgress: core.navigation().routeProgress.map(\.?.routeProgress).eraseToAnyPublisher()) - - super.init() - - observeNavigation() - observeCamera() + init(withMapView: NavigationMapView, navigationProvider: MapboxNavigation) { + self.navigationMapView = withMapView + self.core = navigationProvider } - private func observeNavigation() { - self.core.tripSession().session - .map { - if case .activeGuidance = $0.state { return true } - return false - } - .removeDuplicates() - .assign(to: &$isInActiveNavigation) - - core.navigation().routeProgress.sink { state in - self.routeProgress=state?.routeProgress - if (self.routeProgress != nil) { - self.onNavigationListener?.onRouteProgress(routeProgress: self.routeProgress!.toFLTRouteProgress()) { _ in } - } - } - - core.tripSession().navigationRoutes - .assign(to: &$routes) - - core.navigation().locationMatching.sink { state in - self.currentLocation = state.enhancedLocation - self.onNavigationListener?.onNewLocation(location: state.enhancedLocation.toFLTNavigationLocation()) { _ in } - } - - if (core.navigation().currentLocationMatching != nil) { - self.currentLocation = self.core.navigation().currentLocationMatching?.enhancedLocation - if(self.currentLocation != nil) - { - self.onNavigationListener?.onNewLocation(location: self.currentLocation!.toFLTNavigationLocation()) { _ in } - } - } - } - - private func observeCamera() { - navigationCamera.cameraStates - .sink { [weak self] cameraState in - guard let self else { return } - self.cameraState = cameraState - self.onNavigationListener?.onNavigationCameraStateChanged(state: self.cameraState.toFLTNavigationCameraState()!) {_ in } - } - } - - private var customRouteLineLayerPosition: MapboxMaps.LayerPosition? = nil { - didSet { - mapStyleManager.customRouteLineLayerPosition = customRouteLineLayerPosition - guard let routes else { return } - show(routes, routeAnnotationKinds: routeAnnotationKinds) - } - } - - private(set) var routeAnnotationKinds: Set = [] - - // MARK: - Public configuration - - /// The padding applied to the viewport in addition to the safe area. - public var viewportPadding: UIEdgeInsets = Constants.initialViewportPadding { - didSet { updateCameraPadding() } - } - - /// Controls whether to show annotations on intersections, e.g. traffic signals, railroad crossings, yield and stop - /// signs. Defaults to `true`. - public var showsIntersectionAnnotations: Bool = true { - didSet { - updateIntersectionAnnotations(routeProgress: routeProgress) - } - } - - /// Toggles displaying alternative routes. If enabled, view will draw actual alternative route lines on the map. - /// Defaults to `true`. - public var showsAlternatives: Bool = true { - didSet { - updateAlternatives(routeProgress: routeProgress) - } - } - - /// Toggles displaying relative ETA callouts on alternative routes, during active guidance. - /// Defaults to `true`. - public var showsRelativeDurationsOnAlternativeManuever: Bool = true { - didSet { - if showsRelativeDurationsOnAlternativeManuever { - routeAnnotationKinds = [.relativeDurationsOnAlternativeManuever] - } else { - routeAnnotationKinds.removeAll() - } - updateAlternatives(routeProgress: routeProgress) - } - } - - /// Controls whether the main route style layer and its casing disappears as the user location puck travels over it. - /// Defaults to `true`. - /// - /// If `true`, the part of the route that has been traversed will be rendered with full transparency, to give the - /// illusion of a disappearing route. If `false`, the whole route will be shown without traversed part disappearing - /// effect. - public var routeLineTracksTraversal: Bool = true - - /// The maximum distance (in screen points) the user can tap for a selection to be valid when selecting a POI. - public var poiClickableAreaSize: CGFloat = 40 - - /// Controls whether to show restricted portions of a route line. Defaults to true. - public var showsRestrictedAreasOnRoute: Bool = true - - /// Decreases route line opacity based on occlusion from 3D objects. - /// Value `0` disables occlusion, value `1` means fully occluded. Defaults to `0.85`. - public var routeLineOcclusionFactor: Double = 0.85 - - /// Configuration for displaying congestion levels on the route line. - /// Allows to customize the congestion colors and ranges that represent different congestion levels. - public var congestionConfiguration: CongestionConfiguration = .default - - /// Controls whether the traffic should be drawn on the route line or not. Defaults to true. - public var showsTrafficOnRouteLine: Bool = true - - /// Maximum distance (in screen points) the user can tap for a selection to be valid when selecting an alternate - /// route. - public var tapGestureDistanceThreshold: CGFloat = 50 - - /// Controls whether intermediate waypoints displayed on the route line. Defaults to `true`. - public var showsIntermediateWaypoints: Bool = true { - didSet { - updateWaypointsVisiblity() - } - } - - // MARK: RouteLine Customization - - /// Configures the route line color for the main route. - /// If set, overrides the `.unknown` and `.low` traffic colors. - @objc public dynamic var routeColor: UIColor { - get { - congestionConfiguration.colors.mainRouteColors.unknown - } - set { - congestionConfiguration.colors.mainRouteColors.unknown = newValue - congestionConfiguration.colors.mainRouteColors.low = newValue - } - } - - /// Configures the route line color for alternative routes. - /// If set, overrides the `.unknown` and `.low` traffic colors. - @objc public dynamic var routeAlternateColor: UIColor { - get { - congestionConfiguration.colors.alternativeRouteColors.unknown - } - set { - congestionConfiguration.colors.alternativeRouteColors.unknown = newValue - congestionConfiguration.colors.alternativeRouteColors.low = newValue - } - } - - /// Configures the casing route line color for the main route. - @objc public dynamic var routeCasingColor: UIColor = .defaultRouteCasing - /// Configures the casing route line color for alternative routes. - @objc public dynamic var routeAlternateCasingColor: UIColor = .defaultAlternateLineCasing - /// Configures the color for restricted areas on the route line. - @objc public dynamic var routeRestrictedAreaColor: UIColor = .defaultRouteRestrictedAreaColor - /// Configures the color for the traversed part of the main route. The traversed part is rendered only if the color - /// is not `nil`. - /// Defaults to `nil`. - @objc public dynamic var traversedRouteColor: UIColor? = nil - /// Configures the color of the maneuver arrow. - @objc public dynamic var maneuverArrowColor: UIColor = .defaultManeuverArrow - /// Configures the stroke color of the maneuver arrow. - @objc public dynamic var maneuverArrowStrokeColor: UIColor = .defaultManeuverArrowStroke - - // MARK: Route Annotations Customization - - /// Configures the color of the route annotation for the main route. - @objc public dynamic var routeAnnotationSelectedColor: UIColor = - .defaultSelectedRouteAnnotationColor - /// Configures the color of the route annotation for alternative routes. - @objc public dynamic var routeAnnotationColor: UIColor = .defaultRouteAnnotationColor - /// Configures the text color of the route annotation for the main route. - @objc public dynamic var routeAnnotationSelectedTextColor: UIColor = .defaultSelectedRouteAnnotationTextColor - /// Configures the text color of the route annotation for alternative routes. - @objc public dynamic var routeAnnotationTextColor: UIColor = .defaultRouteAnnotationTextColor - /// Configures the text color of the route annotation for alternative routes when relative duration is greater then - /// the main route. - @objc public dynamic var routeAnnotationMoreTimeTextColor: UIColor = .defaultRouteAnnotationMoreTimeTextColor - /// Configures the text color of the route annotation for alternative routes when relative duration is lesser then - /// the main route. - @objc public dynamic var routeAnnotationLessTimeTextColor: UIColor = .defaultRouteAnnotationLessTimeTextColor - /// Configures the text font of the route annotations. - @objc public dynamic var routeAnnotationTextFont: UIFont = .defaultRouteAnnotationTextFont - /// Configures the waypoint color. - @objc public dynamic var waypointColor: UIColor = .defaultWaypointColor - /// Configures the waypoint stroke color. - @objc public dynamic var waypointStrokeColor: UIColor = .defaultWaypointStrokeColor - - public func update(navigationCameraState: MapboxNavigationCore.NavigationCameraState) { - guard cameraState != navigationCamera.currentCameraState else { return } - navigationCamera.update(cameraState: navigationCameraState) - } - - /// Represents a set of ``RoadAlertType`` values that should be hidden from the map display. - /// By default, this is an empty set, which indicates that all road alerts will be displayed. - /// - /// - Note: If specific `RoadAlertType` values are added to this set, those alerts will be - /// excluded from the map rendering. - public var excludedRouteAlertTypes: RoadAlertType = [] { - didSet { - guard let navigationRoutes = routes else { - return - } - - mapStyleManager.updateRouteAlertsAnnotations( - navigationRoutes: navigationRoutes, - excludedRouteAlertTypes: excludedRouteAlertTypes - ) - } - } - - /// Visualizes the given routes and it's alternatives, removing any existing from the map. - /// - /// Each route is visualized as a line. Each line is color-coded by traffic congestion, if congestion - /// levels are present. To also visualize waypoints and zoom the map to fit, - /// use the ``showcase(_:routesPresentationStyle:routeAnnotationKinds:animated:duration:)`` method. - /// - /// To undo the effects of this method, use ``removeRoutes()`` method. - /// - Parameters: - /// - navigationRoutes: ``NavigationRoutes`` to be displayed on the map. - /// - routeAnnotationKinds: A set of ``RouteAnnotationKind`` that should be displayed. - public func show( - _ navigationRoutes: NavigationRoutes, - routeAnnotationKinds: Set - ) { - removeRoutes() - routes = navigationRoutes - self.routeAnnotationKinds = routeAnnotationKinds - let mainRoute = navigationRoutes.mainRoute.route - if routeLineTracksTraversal { - initPrimaryRoutePoints(route: mainRoute) - } - mapStyleManager.updateRoutes( - navigationRoutes, - config: mapStyleConfig, - featureProvider: customRouteLineFeatureProvider - ) - updateWaypointsVisiblity() - - mapStyleManager.updateRouteAnnotations( - navigationRoutes: navigationRoutes, - annotationKinds: routeAnnotationKinds, - config: mapStyleConfig - ) - mapStyleManager.updateRouteAlertsAnnotations( - navigationRoutes: navigationRoutes, - excludedRouteAlertTypes: excludedRouteAlertTypes - ) - } - - /// Removes routes and all visible annotations from the map. - public func removeRoutes() { - routes = nil - routeLineGranularDistances = nil - routeRemainingDistancesIndex = nil - mapStyleManager.removeAllFeatures() - } - - func updateArrow(routeProgress: MapboxNavigationCore.RouteProgress) { - if routeProgress.currentLegProgress.followOnStep != nil { - mapStyleManager.updateArrows( - route: routeProgress.route, - legIndex: routeProgress.legIndex, - stepIndex: routeProgress.currentLegProgress.stepIndex + 1, - config: mapStyleConfig - ) - } else { - removeArrows() - } - } - - /// Removes the `RouteStep` arrow from the `MapView`. - func removeArrows() { - mapStyleManager.removeArrows() - } - - // MARK: - Camera - - private func updateCameraPadding() { - let padding = viewportPadding - let safeAreaInsets = mapView.safeAreaInsets - - navigationCamera.viewportPadding = .init( - top: safeAreaInsets.top + padding.top, - left: safeAreaInsets.left + padding.left, - bottom: safeAreaInsets.bottom + padding.bottom, - right: safeAreaInsets.right + padding.right - ) - } - - private func fitCamera( - routes: NavigationRoutes, - routesPresentationStyle: RoutesPresentationStyle, - animated: Bool = false, - duration: TimeInterval - ) { - navigationCamera.stop() - let coordinates: [CLLocationCoordinate2D] - switch routesPresentationStyle { - case .main, .all(shouldFit: false): - coordinates = routes.mainRoute.route.shape?.coordinates ?? [] - case .all(true): - let routes = [routes.mainRoute.route] + routes.alternativeRoutes.map(\.route) - coordinates = MultiLineString(routes.compactMap(\.shape?.coordinates)).coordinates.flatMap { $0 } - } - let initialCameraOptions = MapboxMaps.CameraOptions( - padding: navigationCamera.viewportPadding, - bearing: 0, - pitch: 0 - ) - do { - let cameraOptions = try mapView.mapboxMap.camera( - for: coordinates, - camera: initialCameraOptions, - coordinatesPadding: nil, - maxZoom: nil, - offset: nil - ) - mapView.camera.ease(to: cameraOptions, duration: animated ? duration : 0.0) - } catch { - Log.error("Failed to fit the camera: \(error.localizedDescription)", category: .navigationUI) - } - } - - private var customRouteLineFeatureProvider: RouteLineFeatureProvider { - .init { [weak self] identifier, sourceIdentifier in - guard let self else { return nil } - return delegate?.navigationMapView( - self, - routeLineLayerWithIdentifier: identifier, - sourceIdentifier: sourceIdentifier - ) - } customRouteCasingLineLayer: { [weak self] identifier, sourceIdentifier in - guard let self else { return nil } - return delegate?.navigationMapView( - self, - routeCasingLineLayerWithIdentifier: identifier, - sourceIdentifier: sourceIdentifier - ) - } customRouteRestrictedAreasLineLayer: { [weak self] identifier, sourceIdentifier in - guard let self else { return nil } - return delegate?.navigationMapView( - self, - routeRestrictedAreasLineLayerWithIdentifier: identifier, - sourceIdentifier: sourceIdentifier - ) - } - } - - private var waypointsFeatureProvider: WaypointFeatureProvider { - .init { [weak self] waypoints, legIndex in - guard let self else { return nil } - return delegate?.navigationMapView(self, shapeFor: waypoints, legIndex: legIndex) - } customCirleLayer: { [weak self] identifier, sourceIdentifier in - guard let self else { return nil } - return delegate?.navigationMapView( - self, - waypointCircleLayerWithIdentifier: identifier, - sourceIdentifier: sourceIdentifier - ) - } customSymbolLayer: { [weak self] identifier, sourceIdentifier in - guard let self else { return nil } - return delegate?.navigationMapView( - self, - waypointSymbolLayerWithIdentifier: identifier, - sourceIdentifier: sourceIdentifier - ) - } - } - - private func updateWaypointsVisiblity() { - guard let mainRoute = routes?.mainRoute.route else { - mapStyleManager.removeWaypoints() - return - } - - mapStyleManager.updateWaypoints( - route: mainRoute, - legIndex: routeProgress?.legIndex ?? 0, - config: mapStyleConfig, - featureProvider: waypointsFeatureProvider - ) - } - - // MARK: Configuring Cache and Tiles Storage - - private var predictiveCacheMapObserver: MapboxMaps.Cancelable? = nil - - /// Setups the Predictive Caching mechanism using provided Options. - /// - /// This will handle all the required manipulations to enable the feature and maintain it during the navigations. - /// Once enabled, it will be present as long as `NavigationMapView` is retained. - /// - /// - parameter options: options, controlling caching parameters like area radius and concurrent downloading - /// threads. - private func enablePredictiveCaching(with predictiveCacheManager: PredictiveCacheManager?) { - predictiveCacheMapObserver?.cancel() - - guard let predictiveCacheManager else { - predictiveCacheMapObserver = nil - return - } - - predictiveCacheManager.updateMapControllers(mapView: mapView) - predictiveCacheMapObserver = mapView.mapboxMap.onStyleLoaded.observe { [ - weak self, - predictiveCacheManager - ] _ in - guard let self else { return } - - predictiveCacheManager.updateMapControllers(mapView: mapView) - } - } - - private var mapStyleConfig: MapStyleConfig { - .init( - routeCasingColor: routeCasingColor, - routeAlternateCasingColor: routeAlternateCasingColor, - routeRestrictedAreaColor: routeRestrictedAreaColor, - traversedRouteColor: traversedRouteColor, - maneuverArrowColor: maneuverArrowColor, - maneuverArrowStrokeColor: maneuverArrowStrokeColor, - routeAnnotationSelectedColor: routeAnnotationSelectedColor, - routeAnnotationColor: routeAnnotationColor, - routeAnnotationSelectedTextColor: routeAnnotationSelectedTextColor, - routeAnnotationTextColor: routeAnnotationTextColor, - routeAnnotationMoreTimeTextColor: routeAnnotationMoreTimeTextColor, - routeAnnotationLessTimeTextColor: routeAnnotationLessTimeTextColor, - routeAnnotationTextFont: routeAnnotationTextFont, - routeLineTracksTraversal: routeLineTracksTraversal, - isRestrictedAreaEnabled: showsRestrictedAreasOnRoute, - showsTrafficOnRouteLine: showsTrafficOnRouteLine, - showsAlternatives: showsAlternatives, - showsIntermediateWaypoints: showsIntermediateWaypoints, - occlusionFactor: .constant(routeLineOcclusionFactor), - congestionConfiguration: congestionConfiguration, - waypointColor: waypointColor, - waypointStrokeColor: waypointStrokeColor - ) - } func startFreeDrive() { core.tripSession().startFreeDrive() @@ -518,20 +37,21 @@ final class NavigationController: NSObject, NavigationInterface { func cancelPreview() { waypoints = [] routes = nil - update(navigationCameraState: .following) + self.navigationMapView.removeRoutes() } func startActiveNavigation() { guard let previewRoutes = routes else { return } + //self.navigationMapView.route core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) - routes = nil - waypoints = [] - update(navigationCameraState: .following) + //routes = nil + //waypoints = [] + } func stopActiveNavigation() { core.tripSession().startFreeDrive() - update(navigationCameraState: .following) + self.navigationMapView.navigationCamera.stop() } func requestRoutes(points: [Point]) async throws { @@ -557,7 +77,7 @@ final class NavigationController: NSObject, NavigationInterface { routes = previewRoutes self.onNavigationListener?.onNavigationRouteReady() { _ in } } - update(navigationCameraState: .idle) + self.navigationMapView.showcase(routes!) } func addListeners(messenger: SuffixBinaryMessenger) { @@ -583,47 +103,47 @@ final class NavigationController: NSObject, NavigationInterface { func stopTripSession(completion: @escaping (Result) -> Void) { - update(navigationCameraState: .overview) + 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) - update(navigationCameraState: .following) routes = nil waypoints = [] completion(.success(Void())) } func requestNavigationCameraToFollowing(completion: @escaping (Result) -> Void) { - update(navigationCameraState: .following) + //update(navigationCameraState: .following) completion(.success(Void())) } func requestNavigationCameraToOverview(completion: @escaping (Result) -> Void) { - update(navigationCameraState: .overview) + //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 (self.mapView.location.latestLocation != nil) { - let timestamp = Int64(self.mapView.location.latestLocation!.timestamp.timeIntervalSince1970) + else if (mapView.location.latestLocation != nil) { + let timestamp = Int64(mapView.location.latestLocation!.timestamp.timeIntervalSince1970) completion(.success(NavigationLocation( - latitude: self.mapView.location.latestLocation!.coordinate.latitude, - longitude: self.mapView.location.latestLocation!.coordinate.longitude, + latitude: mapView.location.latestLocation!.coordinate.latitude, + longitude: mapView.location.latestLocation!.coordinate.longitude, timestamp: timestamp, monotonicTimestamp: timestamp, - altitude: self.mapView.location.latestLocation!.altitude, - horizontalAccuracy: self.mapView.location.latestLocation!.horizontalAccuracy, - verticalAccuracy: self.mapView.location.latestLocation!.verticalAccuracy, - speed: self.mapView.location.latestLocation!.speed, - speedAccuracy: self.mapView.location.latestLocation!.speedAccuracy, + 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, diff --git a/ios/mapbox_maps_flutter.podspec b/ios/mapbox_maps_flutter.podspec index 9a834b95c..5308742d8 100644 --- a/ios/mapbox_maps_flutter.podspec +++ b/ios/mapbox_maps_flutter.podspec @@ -29,5 +29,5 @@ Pod::Spec.new do |s| s.dependency 'MapboxMaps', '11.8.0' s.dependency 'Turf', '3.0.0' - s.dependency 'MapboxNavigationCoreUnofficial', '3.5.0' + s.dependency 'MapboxNavigationCoreUnofficial', '3.5.1' end From 7924b80cf53cad398c7c402add4dcccf90880900 Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sun, 9 Feb 2025 15:42:49 +0100 Subject: [PATCH 22/33] it works --- example/lib/navigator_example.dart | 20 ++++++--------- ios/Classes/MapboxMapController.swift | 25 +++++++++++-------- ios/Classes/NavigationController.swift | 34 +++++++++++++++++--------- 3 files changed, 46 insertions(+), 33 deletions(-) diff --git a/example/lib/navigator_example.dart b/example/lib/navigator_example.dart index c326dea29..564545788 100644 --- a/example/lib/navigator_example.dart +++ b/example/lib/navigator_example.dart @@ -107,16 +107,12 @@ class NavigatorExampleState extends State 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, - locationPuck: LocationPuck( - locationPuck2D: DefaultLocationPuck2D( - topImage: list, - bearingImage: Uint8List.fromList([]), - shadowImage: Uint8List.fromList([]))))); + //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(); @@ -128,8 +124,8 @@ class NavigatorExampleState extends State return; } - var myCoordinate = Position(lastLocation.longitude!, lastLocation.latitude!); - // } + var myCoordinate = Position(lastLocation.longitude!, lastLocation.latitude!); + //} await mapboxMap .setCamera(CameraOptions(center: Point(coordinates: myCoordinate))); diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index 033dd21f6..cc160a01c 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -18,10 +18,7 @@ final class MapboxMapController: NSObject, FlutterPlatformView { private let navigationController: NavigationController? private let eventHandler: MapboxEventHandler private let binaryMessenger: SuffixBinaryMessenger - private static let navigationProvider: MapboxNavigationProvider = MapboxNavigationProvider(coreConfig: CoreConfig( - credentials: .init(), // You can pass a custom token if you need to, - locationSource: .live - )) + private static var navigationProvider: MapboxNavigationProvider? private var navigationMapView: NavigationMapView! @@ -40,8 +37,18 @@ 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 mapboxNavigation = MapboxMapController.navigationProvider!.mapboxNavigation let navigationMapView = NavigationMapView( location: mapboxNavigation.navigation() .locationMatching.map(\.enhancedLocation) @@ -49,14 +56,14 @@ final class MapboxMapController: NSObject, FlutterPlatformView { routeProgress: mapboxNavigation.navigation() .routeProgress.map(\.?.routeProgress) .eraseToAnyPublisher(), - predictiveCacheManager: MapboxMapController.navigationProvider.predictiveCacheManager, + predictiveCacheManager: MapboxMapController.navigationProvider!.predictiveCacheManager, frame: frame, mapInitOptions: mapInitOptions ) + navigationMapView.viewportPadding = UIEdgeInsets(top: 20, left: 20, bottom: 80, right: 20) navigationMapView.puckType = .puck2D(.navigationDefault) - //navigationMapView.delegate = self - navigationMapView.translatesAutoresizingMaskIntoConstraints = false + navigationMapView.translatesAutoresizingMaskIntoConstraints = true self.navigationMapView = navigationMapView @@ -74,8 +81,6 @@ final class MapboxMapController: NSObject, FlutterPlatformView { channelSuffix: String(channelSuffix) ) - //mapView.location.override(locationProvider: self.navigationProvider, headingProvider: nil) - let styleController = StyleController(styleManager: mapboxMap) StyleManagerSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: styleController, messageChannelSuffix: binaryMessenger.suffix) diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index 078f54b3f..a41c5661e 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -27,9 +27,17 @@ final class NavigationController: NSObject, NavigationInterface { 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 } + } + } + func startFreeDrive() { core.tripSession().startFreeDrive() } @@ -42,11 +50,7 @@ final class NavigationController: NSObject, NavigationInterface { func startActiveNavigation() { guard let previewRoutes = routes else { return } - //self.navigationMapView.route core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) - //routes = nil - //waypoints = [] - } func stopActiveNavigation() { @@ -55,8 +59,7 @@ final class NavigationController: NSObject, NavigationInterface { } func requestRoutes(points: [Point]) async throws { - guard !isInActiveNavigation, let currentLocation else { return } - + self.waypoints = points.map { Waypoint(coordinate: LocationCoordinate2D(latitude: $0.coordinates.latitude, longitude: $0.coordinates.longitude)) } let provider = core.routingProvider() @@ -116,12 +119,12 @@ final class NavigationController: NSObject, NavigationInterface { } func requestNavigationCameraToFollowing(completion: @escaping (Result) -> Void) { - //update(navigationCameraState: .following) + navigationMapView.update(navigationCameraState: .following) completion(.success(Void())) } func requestNavigationCameraToOverview(completion: @escaping (Result) -> Void) { - //update(navigationCameraState: .overview) + navigationMapView.update(navigationCameraState: .overview) completion(.success(Void())) } @@ -131,7 +134,8 @@ final class NavigationController: NSObject, NavigationInterface { { completion(.success(self.currentLocation!.toFLTNavigationLocation())) } - else if (mapView.location.latestLocation != nil) { + else if(mapView.location.latestLocation != nil) + { let timestamp = Int64(mapView.location.latestLocation!.timestamp.timeIntervalSince1970) completion(.success(NavigationLocation( @@ -150,6 +154,14 @@ final class NavigationController: NSObject, NavigationInterface { source: nil ))) } - completion(.success(nil)) + else { + var locationManager = NavigationLocationManager() + if(locationManager.location != nil){ + completion(.success(locationManager.location!.toFLTNavigationLocation())) + } + else{ + completion(.success(nil)) + } + } } } From a442b0b7aebd603ac59898bbff0fde38737fcbd8 Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sun, 9 Feb 2025 15:48:55 +0100 Subject: [PATCH 23/33] events generation --- ios/Classes/NavigationController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index a41c5661e..3a4acc96e 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -36,6 +36,13 @@ final class NavigationController: NSObject, NavigationInterface { 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() { From 336e72c1cc920c3a7d84e010440e0b8c5605d99a Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sun, 9 Feb 2025 15:51:13 +0100 Subject: [PATCH 24/33] fix pod spec --- example/ios/Podfile.lock | 8 ++++---- ios/mapbox_maps_flutter.podspec | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 46c27ff5a..ae19ecc04 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -5,7 +5,7 @@ PODS: - mapbox_maps_flutter (2.4.0): - Flutter - MapboxMaps (= 11.8.0) - - MapboxNavigationCoreUnofficial (= 3.5.1) + - MapboxNavigationCoreUnofficial (= 3.5.3) - Turf (= 3.0.0) - MapboxCommon (24.8.0) - MapboxCoreMaps (11.8.0): @@ -16,7 +16,7 @@ PODS: - MapboxCommon (= 24.8.0) - MapboxCoreMaps (= 11.8.0) - Turf (= 3.0.0) - - MapboxNavigationCoreUnofficial (3.5.1): + - MapboxNavigationCoreUnofficial (3.5.3): - MapboxDirectionsUnofficial (= 3.5.0) - MapboxMaps (= 11.8.0) - MapboxNavigationHelpersUnofficial (= 3.5.0) @@ -64,12 +64,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - mapbox_maps_flutter: ec1446f389200627f54e67adc8a854036724ddad + mapbox_maps_flutter: 35092c544fbcf2e79372e9f0011286749b4a47d5 MapboxCommon: 95fe03b74d0d0ca39dc646ca14862deb06875151 MapboxCoreMaps: f2a82182c5f6c6262220b81547c6df708012932b MapboxDirectionsUnofficial: 4244d39727c60672e45800784e121782d55a60ad MapboxMaps: dbe1869006c5918d62efc6b475fb884947ea2ecd - MapboxNavigationCoreUnofficial: 76a196139b4ad56ab47006404130401eacd51a44 + MapboxNavigationCoreUnofficial: 5cacabfe1ea507be11f84162cb7cde7eaf439f9b MapboxNavigationHelpersUnofficial: 325ef24b1487c336572dad217e35a41be8199eae MapboxNavigationNative: 3a300f654f9673c6e4cc5f6743997cc3a4c5ceae path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 diff --git a/ios/mapbox_maps_flutter.podspec b/ios/mapbox_maps_flutter.podspec index 5308742d8..d92736092 100644 --- a/ios/mapbox_maps_flutter.podspec +++ b/ios/mapbox_maps_flutter.podspec @@ -29,5 +29,5 @@ Pod::Spec.new do |s| s.dependency 'MapboxMaps', '11.8.0' s.dependency 'Turf', '3.0.0' - s.dependency 'MapboxNavigationCoreUnofficial', '3.5.1' + s.dependency 'MapboxNavigationCoreUnofficial', '3.5.3' end From 99e5c3e0619149ee3346e56b707b34268f74bbb8 Mon Sep 17 00:00:00 2001 From: Oleg Krymskyi Date: Sun, 9 Feb 2025 15:58:07 +0100 Subject: [PATCH 25/33] rollback access token --- example/integration_test/empty_map_widget.dart | 2 +- example/ios/Runner/Info.plist | 2 +- example/lib/main.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/integration_test/empty_map_widget.dart b/example/integration_test/empty_map_widget.dart index ab8ed174c..e5ae7dcf2 100644 --- a/example/integration_test/empty_map_widget.dart +++ b/example/integration_test/empty_map_widget.dart @@ -22,7 +22,7 @@ class Events { } var events = Events(); -const ACCESS_TOKEN = "pk.eyJ1IjoicmlkZWhpa2UiLCJhIjoiY2xwc2wwNGZrMDN3eTJqcGwxdjViaGRzdiJ9.jFtcT-N-qh-Zj7i0vrWxAA"; +const ACCESS_TOKEN = String.fromEnvironment('ACCESS_TOKEN'); Future main({double? width, double? height, CameraOptions? camera}) { final completer = Completer(); diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 8f8e35d8d..bef02a4ac 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -25,7 +25,7 @@ LSRequiresIPhoneOS MBXAccessToken - pk.eyJ1IjoicmlkZWhpa2UiLCJhIjoiY2xwc2wwNGZrMDN3eTJqcGwxdjViaGRzdiJ9.jFtcT-N-qh-Zj7i0vrWxAA + YOUR_ACCESS_TOKEN NSLocationAlwaysAndWhenInUseUsageDescription Always and when in use! NSLocationAlwaysUsageDescription diff --git a/example/lib/main.dart b/example/lib/main.dart index 3230f2703..81b5043d8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -64,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 = "pk.eyJ1IjoicmlkZWhpa2UiLCJhIjoiY2xwc2wwNGZrMDN3eTJqcGwxdjViaGRzdiJ9.jFtcT-N-qh-Zj7i0vrWxAA"; + static const String ACCESS_TOKEN = String.fromEnvironment("ACCESS_TOKEN"); void _pushPage(BuildContext context, Example page) async { Navigator.of(context).push(MaterialPageRoute( From 57aad5b6c5a379cdada04c9e0e2c934a503fad16 Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Wed, 12 Feb 2025 23:43:33 +0100 Subject: [PATCH 26/33] android latest location --- android/build.gradle | 1 + android/src/main/AndroidManifest.xml | 2 ++ .../maps/mapbox_maps/NavigationController.kt | 18 +++++++++++++++--- .../mapbox_maps/mapping/NavigationMappings.kt | 8 ++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 646a9b0ec..678e2efbd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -84,4 +84,5 @@ dependencies { implementation "androidx.annotation:annotation:1.5.0" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" + implementation "com.google.android.gms:play-services-location:20.0.0" } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index a2f47b605..44ce175db 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,2 +1,4 @@ + + 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 index dc3cf5309..286425944 100644 --- a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt @@ -7,6 +7,8 @@ import android.content.res.Resources 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 @@ -334,11 +336,21 @@ class NavigationController( override fun lastLocation(callback: (Result) -> Unit) { val point = this.navigationLocationProvider.lastLocation - if (point == null) { - callback.invoke(Result.success(null)) + if (point != null) { + callback.invoke(Result.success(point.toFLT())) return } - callback.invoke(Result.success(point.toFLT())) + 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 { exception -> + 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 index 48550a662..d24775589 100644 --- 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 @@ -31,6 +31,14 @@ fun Location.toFLT(): NavigationLocation { ) } +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, From a22320bc508d2d738ee7b6bd6288a2fdae57cf34 Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Fri, 28 Feb 2025 11:37:29 +0100 Subject: [PATCH 27/33] ndk27 support --- android/build.gradle | 42 +++++++++++++++---- android/gradle.properties | 2 +- .../maps/mapbox_maps/NavigationController.kt | 27 +++++++++--- example/android/app/build.gradle | 6 +-- example/lib/navigator_example.dart | 16 +++---- 5 files changed, 68 insertions(+), 25 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 678e2efbd..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,11 +78,37 @@ if (file("$rootDir/gradle/ktlint.gradle").exists() && file("$rootDir/gradle/lint } dependencies { - implementation "com.mapbox.maps:android:11.8.0" - implementation "com.mapbox.navigationcore:navigation:3.5.0-beta.1" - implementation "com.mapbox.navigationcore:ui-components:3.3.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:20.0.0" + 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/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt index 286425944..03f48d70e 100644 --- a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt @@ -4,6 +4,7 @@ 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 @@ -39,10 +40,14 @@ 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.camera.state.NavigationCameraState +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, @@ -118,7 +123,7 @@ class NavigationController( private val routeProgressObserver = RouteProgressObserver { routeProgress -> // update the camera position to account for the progressed fragment of the route viewportDataSource.onRouteProgressChanged(routeProgress) - viewportDataSource.evaluate() + //viewportDataSource.evaluate() this.fltNavigationListener?.onRouteProgress(routeProgress.toFLT()) {} } @@ -140,7 +145,11 @@ class NavigationController( 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) + mapboxNavigation?.setNavigationRoutes(routes) { data -> + if (data.isError) { + println(data.error) + } + } // enable the camera back navigationCamera.requestNavigationCameraToOverview() } @@ -208,7 +217,7 @@ class NavigationController( // update camera position to account for new location viewportDataSource.onLocationChanged(enhancedLocation) - viewportDataSource.evaluate() + //viewportDataSource.evaluate() fltNavigationListener?.onNewLocation(enhancedLocation.toFLT()) {} @@ -234,7 +243,15 @@ class NavigationController( if (mapView.mapboxMap.style != null) { routeLineView.renderRouteDrawData( mapView.mapboxMap.style!!, - this + this, + mapView.mapboxMap, + object : RoutesRenderedCallback { + override fun onRoutesRendered( + result: RoutesRenderedResult + ) { + println(result) + } + } ) } } @@ -349,7 +366,7 @@ class NavigationController( } else { callback.invoke(Result.success(null)) } - }.addOnFailureListener { exception -> + }.addOnFailureListener { _ -> callback.invoke(Result.success(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/lib/navigator_example.dart b/example/lib/navigator_example.dart index 564545788..361501a0b 100644 --- a/example/lib/navigator_example.dart +++ b/example/lib/navigator_example.dart @@ -113,18 +113,18 @@ class NavigatorExampleState extends State // 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; - } + 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!); + var myCoordinate = + Position(lastLocation.longitude!, lastLocation.latitude!); //} await mapboxMap From b0d323a34cc57ae2cc99c4d269fbb497d4b70ca5 Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Mon, 3 Mar 2025 19:01:41 +0100 Subject: [PATCH 28/33] fix --- .../kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 03f48d70e..9f883d0a4 100644 --- a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt @@ -42,7 +42,6 @@ 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.camera.state.NavigationCameraState 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 @@ -217,7 +216,7 @@ class NavigationController( // update camera position to account for new location viewportDataSource.onLocationChanged(enhancedLocation) - //viewportDataSource.evaluate() + viewportDataSource.evaluate() fltNavigationListener?.onNewLocation(enhancedLocation.toFLT()) {} From 48f84c18b1b35d1ae3361d13ba9eeb565511a7f7 Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Mon, 3 Mar 2025 21:14:26 +0100 Subject: [PATCH 29/33] navigation route --- .../mapbox_maps/mapping/NavigationMappings.kt | 6 ++++ .../mapbox_maps/pigeons/NavigationMessager.kt | 33 ++++++++++--------- ios/Classes/Extensions.swift | 1 + .../Generated/NavigationMessager.swift | 32 ++++++++++-------- lib/src/pigeons/navigation.dart | 33 +++++++++++-------- 5 files changed, 62 insertions(+), 43 deletions(-) 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 index d24775589..6f0bc3af7 100644 --- 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 @@ -1,7 +1,12 @@ package com.mapbox.maps.mapbox_maps.mapping +import com.google.gson.GsonBuilder +import com.mapbox.api.directions.v5.DirectionsAdapterFactory import com.mapbox.common.location.Location +import com.mapbox.geojson.Point +import com.mapbox.geojson.PointAsCoordinatesTypeAdapter import com.mapbox.maps.mapbox_maps.pigeons.NavigationLocation +import com.mapbox.navigation.base.route.NavigationRoute.SerialisationState 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 @@ -65,6 +70,7 @@ fun UpcomingRoadObject.toFLT(): NavigationUpcomingRoadObject { 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) }, 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 index 7eda8f255..824be330a 100644 --- 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 @@ -215,6 +215,7 @@ data class UpcomingRoadObject ( /** 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, @@ -233,25 +234,27 @@ data class RouteProgress ( { companion object { fun fromList(pigeonVar_list: List): RouteProgress { - val bannerInstructionsJson = pigeonVar_list[0] as String? - val voiceInstructionsJson = pigeonVar_list[1] as String? - val currentState = pigeonVar_list[2] as RouteProgressState? - val inTunnel = pigeonVar_list[3] as Boolean? - val distanceRemaining = pigeonVar_list[4] as Double? - val distanceTraveled = pigeonVar_list[5] as Double? - val durationRemaining = pigeonVar_list[6] as Double? - val fractionTraveled = pigeonVar_list[7] as Double? - val remainingWaypoints = pigeonVar_list[8] as Long? - val upcomingRoadObjects = pigeonVar_list[9] as List? - val stale = pigeonVar_list[10] as Boolean? - val routeAlternativeId = pigeonVar_list[11] as String? - val currentRouteGeometryIndex = pigeonVar_list[12] as Long? - val inParkingAisle = pigeonVar_list[13] as Boolean? - return RouteProgress(bannerInstructionsJson, voiceInstructionsJson, currentState, inTunnel, distanceRemaining, distanceTraveled, durationRemaining, fractionTraveled, remainingWaypoints, upcomingRoadObjects, stale, routeAlternativeId, currentRouteGeometryIndex, inParkingAisle) + 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, diff --git a/ios/Classes/Extensions.swift b/ios/Classes/Extensions.swift index 6860210f0..ecf9ffb4b 100644 --- a/ios/Classes/Extensions.swift +++ b/ios/Classes/Extensions.swift @@ -1124,6 +1124,7 @@ extension MapboxNavigationCore.RouteProgress { func toFLTRouteProgress() -> RouteProgress { return RouteProgress( + navigationRouteJson: nil, bannerInstructionsJson: nil, voiceInstructionsJson: nil, currentState: .uNCERTAIN, diff --git a/ios/Classes/Generated/NavigationMessager.swift b/ios/Classes/Generated/NavigationMessager.swift index d0dfdfdc0..dd355bd3a 100644 --- a/ios/Classes/Generated/NavigationMessager.swift +++ b/ios/Classes/Generated/NavigationMessager.swift @@ -254,6 +254,7 @@ struct UpcomingRoadObject { /// 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 @@ -273,22 +274,24 @@ struct RouteProgress { // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> RouteProgress? { - let bannerInstructionsJson: String? = nilOrValue(pigeonVar_list[0]) - let voiceInstructionsJson: String? = nilOrValue(pigeonVar_list[1]) - let currentState: RouteProgressState? = nilOrValue(pigeonVar_list[2]) - let inTunnel: Bool? = nilOrValue(pigeonVar_list[3]) - let distanceRemaining: Double? = nilOrValue(pigeonVar_list[4]) - let distanceTraveled: Double? = nilOrValue(pigeonVar_list[5]) - let durationRemaining: Double? = nilOrValue(pigeonVar_list[6]) - let fractionTraveled: Double? = nilOrValue(pigeonVar_list[7]) - let remainingWaypoints: Int64? = nilOrValue(pigeonVar_list[8]) - let upcomingRoadObjects: [UpcomingRoadObject]? = nilOrValue(pigeonVar_list[9]) - let stale: Bool? = nilOrValue(pigeonVar_list[10]) - let routeAlternativeId: String? = nilOrValue(pigeonVar_list[11]) - let currentRouteGeometryIndex: Int64? = nilOrValue(pigeonVar_list[12]) - let inParkingAisle: Bool? = nilOrValue(pigeonVar_list[13]) + 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, @@ -307,6 +310,7 @@ struct RouteProgress { } func toList() -> [Any?] { return [ + navigationRouteJson, bannerInstructionsJson, voiceInstructionsJson, currentState, diff --git a/lib/src/pigeons/navigation.dart b/lib/src/pigeons/navigation.dart index cb9085c69..d28159a30 100644 --- a/lib/src/pigeons/navigation.dart +++ b/lib/src/pigeons/navigation.dart @@ -125,6 +125,7 @@ class UpcomingRoadObject { class RouteProgress { RouteProgress({ + this.navigationRouteJson, this.bannerInstructionsJson, this.voiceInstructionsJson, this.currentState, @@ -141,6 +142,8 @@ class RouteProgress { this.inParkingAisle, }); + String? navigationRouteJson; + String? bannerInstructionsJson; String? voiceInstructionsJson; @@ -171,6 +174,7 @@ class RouteProgress { Object encode() { return [ + navigationRouteJson, bannerInstructionsJson, voiceInstructionsJson, currentState, @@ -191,21 +195,22 @@ class RouteProgress { static RouteProgress decode(Object result) { result as List; return RouteProgress( - bannerInstructionsJson: result[0] as String?, - voiceInstructionsJson: result[1] as String?, - currentState: result[2] as RouteProgressState?, - inTunnel: result[3] as bool?, - distanceRemaining: result[4] as double?, - distanceTraveled: result[5] as double?, - durationRemaining: result[6] as double?, - fractionTraveled: result[7] as double?, - remainingWaypoints: result[8] as int?, + 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[9] as List?)?.cast(), - stale: result[10] as bool?, - routeAlternativeId: result[11] as String?, - currentRouteGeometryIndex: result[12] as int?, - inParkingAisle: result[13] as bool?, + (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?, ); } } From fc85def31113ccb8626b78627e8fa30746131f8f Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Tue, 4 Mar 2025 14:24:01 +0100 Subject: [PATCH 30/33] route options --- .../maps/mapbox_maps/NavigationController.kt | 40 +++++---- .../mapbox_maps/mapping/NavigationMappings.kt | 5 -- .../mapbox_maps/pigeons/NavigationMessager.kt | 84 ++++++++++++++++--- 3 files changed, 96 insertions(+), 33 deletions(-) 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 index 9f883d0a4..425929b9b 100644 --- a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt @@ -200,7 +200,6 @@ class NavigationController( routeLineView = MapboxRouteLineView(mapboxRouteLineOptions) locationObserver = object : LocationObserver { - var firstLocationUpdateReceived = false override fun onNewRawLocation(rawLocation: Location) { // not handled @@ -219,17 +218,6 @@ class NavigationController( viewportDataSource.evaluate() fltNavigationListener?.onNewLocation(enhancedLocation.toFLT()) {} - - // if this is the first location update the activity has received, - // it's best to immediately move the camera to the current user location - if (!firstLocationUpdateReceived) { - firstLocationUpdateReceived = true - navigationCamera.requestNavigationCameraToOverview( - stateTransitionOptions = NavigationCameraTransitionOptions.Builder() - .maxDuration(0) // instant transition - .build() - ) - } } } @@ -291,13 +279,29 @@ class NavigationController( } @SuppressLint("MissingPermission") - override fun setRoute(waypoints: List, callback: (Result) -> Unit) { + 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( - RouteOptions.builder() - .applyDefaultNavigationOptions() - .alternatives(false) - .coordinatesList(waypoints) - .build(), + routeBuilder.build(), object : NavigationRouterCallback { override fun onRoutesReady( 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 index 6f0bc3af7..e232101f7 100644 --- 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 @@ -1,12 +1,7 @@ package com.mapbox.maps.mapbox_maps.mapping -import com.google.gson.GsonBuilder -import com.mapbox.api.directions.v5.DirectionsAdapterFactory import com.mapbox.common.location.Location -import com.mapbox.geojson.Point -import com.mapbox.geojson.PointAsCoordinatesTypeAdapter import com.mapbox.maps.mapbox_maps.pigeons.NavigationLocation -import com.mapbox.navigation.base.route.NavigationRoute.SerialisationState 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 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 index 824be330a..ae504c032 100644 --- 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 @@ -272,6 +272,52 @@ data class RouteProgress ( ) } } +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) { @@ -325,6 +371,16 @@ private open class NavigationMessagerPigeonCodec : StandardMessageCodec() { 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) } } @@ -370,6 +426,14 @@ private open class NavigationMessagerPigeonCodec : StandardMessageCodec() { 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) } } @@ -398,7 +462,7 @@ class NavigationListener(private val binaryMessenger: BinaryMessenger, private v } } else { callback(Result.failure(createConnectionError(channelName))) - } + } } } fun onNavigationRouteFailed(callback: (Result) -> Unit) @@ -415,7 +479,7 @@ class NavigationListener(private val binaryMessenger: BinaryMessenger, private v } } else { callback(Result.failure(createConnectionError(channelName))) - } + } } } fun onNavigationRouteCancelled(callback: (Result) -> Unit) @@ -432,7 +496,7 @@ class NavigationListener(private val binaryMessenger: BinaryMessenger, private v } } else { callback(Result.failure(createConnectionError(channelName))) - } + } } } fun onNavigationRouteRendered(callback: (Result) -> Unit) @@ -449,7 +513,7 @@ class NavigationListener(private val binaryMessenger: BinaryMessenger, private v } } else { callback(Result.failure(createConnectionError(channelName))) - } + } } } fun onNewLocation(locationArg: NavigationLocation, callback: (Result) -> Unit) @@ -466,7 +530,7 @@ class NavigationListener(private val binaryMessenger: BinaryMessenger, private v } } else { callback(Result.failure(createConnectionError(channelName))) - } + } } } fun onRouteProgress(routeProgressArg: RouteProgress, callback: (Result) -> Unit) @@ -483,7 +547,7 @@ class NavigationListener(private val binaryMessenger: BinaryMessenger, private v } } else { callback(Result.failure(createConnectionError(channelName))) - } + } } } fun onNavigationCameraStateChanged(stateArg: NavigationCameraState, callback: (Result) -> Unit) @@ -500,13 +564,13 @@ class NavigationListener(private val binaryMessenger: BinaryMessenger, private v } } else { callback(Result.failure(createConnectionError(channelName))) - } + } } } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface NavigationInterface { - fun setRoute(waypoints: List, callback: (Result) -> Unit) + fun setRoute(options: RouteOptions, callback: (Result) -> Unit) fun stopTripSession(callback: (Result) -> Unit) fun startTripSession(withForegroundService: Boolean, callback: (Result) -> Unit) fun requestNavigationCameraToFollowing(callback: (Result) -> Unit) @@ -527,8 +591,8 @@ interface NavigationInterface { if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List - val waypointsArg = args[0] as List - api.setRoute(waypointsArg) { result: Result -> + val routeOptionsArg = args[0] as RouteOptions + api.setRoute(routeOptionsArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { reply.reply(wrapError(error)) From 550dfb81ef9b18140e8a0d8eec42ddeab3f69725 Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Tue, 4 Mar 2025 14:51:45 +0100 Subject: [PATCH 31/33] route options ios --- .../Generated/NavigationMessager.swift | 68 ++++++++++++++++++- ios/Classes/NavigationController.swift | 13 ++-- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/ios/Classes/Generated/NavigationMessager.swift b/ios/Classes/Generated/NavigationMessager.swift index dd355bd3a..8ff2a6b15 100644 --- a/ios/Classes/Generated/NavigationMessager.swift +++ b/ios/Classes/Generated/NavigationMessager.swift @@ -329,6 +329,62 @@ struct RouteProgress { } } +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 { @@ -364,6 +420,10 @@ private class NavigationMessagerPigeonCodecReader: FlutterStandardReader { 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) } @@ -402,6 +462,12 @@ private class NavigationMessagerPigeonCodecWriter: FlutterStandardWriter { } 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) } @@ -572,7 +638,7 @@ class NavigationListener: NavigationListenerProtocol { } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol NavigationInterface { - func setRoute(waypoints: [Point], completion: @escaping (Result) -> Void) + 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) diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift index 3a4acc96e..512bd75c0 100644 --- a/ios/Classes/NavigationController.swift +++ b/ios/Classes/NavigationController.swift @@ -16,7 +16,7 @@ final class NavigationController: NSObject, NavigationInterface { @Published var profileIdentifier: ProfileIdentifier = .automobileAvoidingTraffic @Published var shouldRequestMapMatching = false - private var waypoints: [Waypoint] = [] + private var waypoints: [MapboxNavigationCore.Waypoint] = [] private let core: MapboxNavigation private var cancelables: Set = [] @@ -65,9 +65,12 @@ final class NavigationController: NSObject, NavigationInterface { self.navigationMapView.navigationCamera.stop() } - func requestRoutes(points: [Point]) async throws { + func requestRoutes(points: [Waypoint]) async throws { - self.waypoints = points.map { Waypoint(coordinate: LocationCoordinate2D(latitude: $0.coordinates.latitude, longitude: $0.coordinates.longitude)) } + self.waypoints = points.map { + MapboxNavigationCore.Waypoint( + coordinate: LocationCoordinate2D(latitude: $0.point!.coordinates.latitude, longitude: $0.point!.coordinates.longitude)) + } let provider = core.routingProvider() if shouldRequestMapMatching { @@ -99,10 +102,10 @@ final class NavigationController: NSObject, NavigationInterface { cancelables = [] } - func setRoute(waypoints: [Point], completion: @escaping (Result) -> Void) { + func setRoute(options: RouteOptions, completion: @escaping (Result) -> Void) { Task { do { - try await self.requestRoutes(points: waypoints) + try await self.requestRoutes(points: options.waypoints!) completion(.success(Void())) } catch { From d1a0a542013fa7d6e6dfcf4db4f88af0e45d611b Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Tue, 4 Mar 2025 22:43:04 +0100 Subject: [PATCH 32/33] fix example --- example/lib/navigator_example.dart | 8 ++-- lib/src/pigeons/navigation.dart | 69 +++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/example/lib/navigator_example.dart b/example/lib/navigator_example.dart index 361501a0b..5728a84c3 100644 --- a/example/lib/navigator_example.dart +++ b/example/lib/navigator_example.dart @@ -133,10 +133,10 @@ class NavigatorExampleState extends State final destinationCoordinate = createRandomPositionAround(myCoordinate); - await mapboxMap.navigation.setRoute([ - Point(coordinates: myCoordinate), - Point(coordinates: destinationCoordinate) - ]); + await mapboxMap.navigation.setRoute(RouteOptions(waypoints: [ + Waypoint(point: Point(coordinates: myCoordinate)), + Waypoint(point: Point(coordinates: destinationCoordinate)) + ])); await mapboxMap.navigation.startTripSession(true); } diff --git a/lib/src/pigeons/navigation.dart b/lib/src/pigeons/navigation.dart index d28159a30..107833f6c 100644 --- a/lib/src/pigeons/navigation.dart +++ b/lib/src/pigeons/navigation.dart @@ -296,6 +296,65 @@ class NavigationLocation { } } +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 @@ -333,6 +392,12 @@ class Navigation_PigeonCodec extends StandardMessageCodec { } 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); } @@ -595,7 +660,7 @@ class NavigationInterface { final String pigeonVar_messageChannelSuffix; - Future setRoute(List waypoints) async { + Future setRoute(RouteOptions options) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.setRoute$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -605,7 +670,7 @@ class NavigationInterface { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([waypoints]) as List?; + await pigeonVar_channel.send([options]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { From bbc18984a2382c0bf2e8c4a2e2972bb14f01a75d Mon Sep 17 00:00:00 2001 From: OlegKrymskyi Date: Tue, 4 Mar 2025 23:06:46 +0100 Subject: [PATCH 33/33] fix ios build --- ios/Classes/Generated/NavigationMessager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Classes/Generated/NavigationMessager.swift b/ios/Classes/Generated/NavigationMessager.swift index 8ff2a6b15..77580ae0e 100644 --- a/ios/Classes/Generated/NavigationMessager.swift +++ b/ios/Classes/Generated/NavigationMessager.swift @@ -656,8 +656,8 @@ class NavigationInterfaceSetup { if let api = api { setRouteChannel.setMessageHandler { message, reply in let args = message as! [Any?] - let waypointsArg = args[0] as! [Point] - api.setRoute(waypoints: waypointsArg) { result in + let routeOptionsArg = args[0] as! RouteOptions + api.setRoute(options: routeOptionsArg) { result in switch result { case .success: reply(wrapResult(nil))