From b4e928b2938315494480563aa0cb52dd0663b36f Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 21:19:38 -0700 Subject: [PATCH 01/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- Package.swift | 2 +- android/composeui/build.gradle | 6 +- android/core/build.gradle | 6 +- .../ferrostar/core/FerrostarCore.kt | 63 ++++++--- .../ferrostar/core/NavigationViewModel.kt | 87 ++++++------ .../core/extensions/TripStateExtensions.kt | 43 ++++++ .../core/mock/MockNavigationState.kt | 10 ++ android/demo-app/build.gradle | 4 +- .../com/stadiamaps/ferrostar/AppModule.kt | 5 + .../ferrostar/DemoNavigationScene.kt | 13 +- android/maplibreui/build.gradle | 6 +- .../views/LandscapeNavigationView.kt | 9 +- .../views/PortraitNavigationView.kt | 9 +- .../Sources/FerrostarCore/FerrostarCore.swift | 74 +++++----- .../Mock/MockNavigationState.swift | 128 ++++++++++-------- .../FerrostarCore/NavigationState.swift | 63 +++++---- .../MapLibre Extensions.swift | 4 +- .../DynamicallyOrientingNavigationView.swift | 12 +- .../Views/LandscapeNavigationView.swift | 12 +- .../Views/NavigationMapView.swift | 14 +- .../LandscapeNavigationOverlayView.swift | 10 +- .../PortraitNavigationOverlayView.swift | 10 +- .../Views/PortraitNavigationView.swift | 12 +- apple/Sources/UniFFI/ferrostar.swift | 20 ++- .../FerrostarCoreTests.swift | 11 +- .../ValhallaCoreTests.swift | 3 +- .../src/navigation_controller/mod.rs | 3 + .../src/navigation_controller/models.rs | 3 + 28 files changed, 399 insertions(+), 243 deletions(-) create mode 100644 android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/TripStateExtensions.kt diff --git a/Package.swift b/Package.swift index 06e2af6c..2570780e 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let binaryTarget: Target let maplibreSwiftUIDSLPackage: Package.Dependency -let useLocalFramework = false +let useLocalFramework = true let useLocalMapLibreSwiftUIDSL = false if useLocalFramework { diff --git a/android/composeui/build.gradle b/android/composeui/build.gradle index b24da458..2db34af8 100644 --- a/android/composeui/build.gradle +++ b/android/composeui/build.gradle @@ -11,7 +11,7 @@ android { compileSdk 34 defaultConfig { - minSdk 25 + minSdk 26 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -24,7 +24,7 @@ android { } } compileOptions { - coreLibraryDesugaringEnabled true +// coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -44,7 +44,7 @@ android { dependencies { // For as long as we support API 25; once we can raise support to 26, all is fine - coreLibraryDesugaring libs.desugar.jdk.libs +// coreLibraryDesugaring libs.desugar.jdk.libs implementation platform(libs.kotlin.bom) diff --git a/android/core/build.gradle b/android/core/build.gradle index fa5b5695..c8974997 100644 --- a/android/core/build.gradle +++ b/android/core/build.gradle @@ -11,7 +11,7 @@ android { ndkVersion "26.2.11394342" defaultConfig { - minSdk 25 + minSdk 26 targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -25,7 +25,7 @@ android { } } compileOptions { - coreLibraryDesugaringEnabled true +// coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -43,7 +43,7 @@ android { dependencies { // For as long as we support API 25; once we can raise support to 26, all is fine - coreLibraryDesugaring libs.desugar.jdk.libs +// coreLibraryDesugaring libs.desugar.jdk.libs implementation libs.androidx.ktx implementation libs.androidx.appcompat diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt index d01c13a2..7b8eade7 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt @@ -9,6 +9,8 @@ import java.util.concurrent.Executors import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import okhttp3.OkHttpClient @@ -29,10 +31,10 @@ import uniffi.ferrostar.Waypoint /** Represents the complete state of the navigation session provided by FerrostarCore-RS. */ data class NavigationState( /** The raw trip state from the core. */ - val tripState: TripState, - val routeGeometry: List, + val tripState: TripState = TripState.Idle, + val routeGeometry: List = emptyList(), /** Indicates when the core is calculating a new route (ex: due to the user being off route). */ - val isCalculatingNewRoute: Boolean + val isCalculatingNewRoute: Boolean = false ) { companion object } @@ -57,6 +59,7 @@ class FerrostarCore( val routeProvider: RouteProvider, val httpClient: OkHttpClient, val locationProvider: LocationProvider, + navigationControllerConfig: NavigationControllerConfig, ) : LocationUpdateListener { companion object { private const val TAG = "FerrostarCore" @@ -112,18 +115,26 @@ class FerrostarCore( private val _executor = Executors.newSingleThreadScheduledExecutor() private val _scope = CoroutineScope(Dispatchers.IO) private var _navigationController: NavigationController? = null - private var _state: MutableStateFlow? = null + private var _state: MutableStateFlow = MutableStateFlow(NavigationState()) private var _routeRequestInFlight = false private var _lastAutomaticRecalculation: Long? = null private var _lastLocation: UserLocation? = null - private var _config: NavigationControllerConfig? = null + private var _config: NavigationControllerConfig = navigationControllerConfig + + /** + * The current state of the navigation session. This can be used in a custom ViewModel or + * elsewhere. If using the default behavior, use the NavigationViewModel provided by + * startNavigation(). + */ + var state: StateFlow = _state.asStateFlow() constructor( valhallaEndpointURL: URL, profile: String, httpClient: OkHttpClient, locationProvider: LocationProvider, + navigationControllerConfig: NavigationControllerConfig, costingOptions: Map = emptyMap(), ) : this( RouteProvider.RouteAdapter( @@ -131,27 +142,29 @@ class FerrostarCore( valhallaEndpointURL.toString(), profile, jsonAdapter.toJson(costingOptions))), httpClient, locationProvider, - ) + navigationControllerConfig) constructor( routeAdapter: RouteAdapter, httpClient: OkHttpClient, locationProvider: LocationProvider, + navigationControllerConfig: NavigationControllerConfig, ) : this( RouteProvider.RouteAdapter(routeAdapter), httpClient, locationProvider, - ) + navigationControllerConfig) constructor( customRouteProvider: CustomRouteProvider, - httpClient: OkHttpClient, + httpClient: OkHttpClient = OkHttpClient(), locationProvider: LocationProvider, + navigationControllerConfig: NavigationControllerConfig, ) : this( RouteProvider.CustomProvider(customRouteProvider), httpClient, locationProvider, - ) + navigationControllerConfig) suspend fun getRoutes(initialLocation: UserLocation, waypoints: List): List = try { @@ -198,31 +211,42 @@ class FerrostarCore( * WARNING: If you want to reuse the existing view model, ex: when getting a new route after going * off course, use [replaceRoute] instead! Otherwise, you will miss out on updates as the old view * model is "orphaned"! + * + * @param route the route to navigate. + * @param config Override the configuration for the navigation session. This was provided on init. + * @return a view model tied to the navigation session. This can be ignored if you're injecting + * the [NavigationViewModel]/[DefaultNavigationViewModel]. + * @throws UserLocationUnknown if the location provider has no last known location. */ @Throws(UserLocationUnknown::class) - fun startNavigation(route: Route, config: NavigationControllerConfig): NavigationViewModel { + fun startNavigation( + route: Route, + config: NavigationControllerConfig? = null + ): NavigationViewModel { stopNavigation() + // Apply the new config if provided, otherwise use the original. + _config = config ?: _config + val controller = NavigationController( route, - config, + _config, ) val startingLocation = locationProvider.lastLocation ?: UserLocation(route.geometry.first(), 0.0, null, Instant.now(), null) val initialTripState = controller.getInitialState(startingLocation) - val stateFlow = - MutableStateFlow(NavigationState(tripState = initialTripState, route.geometry, false)) + val newState = NavigationState(tripState = initialTripState, route.geometry, false) handleStateUpdate(initialTripState, startingLocation) _navigationController = controller - _state = stateFlow + _state.value = newState locationProvider.addListener(this, _executor) - return NavigationViewModel(stateFlow, startingLocation) + return DefaultNavigationViewModel(this, locationProvider) } /** @@ -242,10 +266,7 @@ class FerrostarCore( ?: UserLocation(route.geometry.first(), 0.0, null, Instant.now(), null) _navigationController = controller - if (_state == null) { - android.util.Log.e(TAG, "Unexpected null state") - } - _state?.update { + _state.update { val newState = controller.getInitialState(startingLocation) handleStateUpdate(newState, startingLocation) @@ -259,7 +280,7 @@ class FerrostarCore( val location = _lastLocation if (controller != null && location != null) { - _state?.update { currentValue -> + _state.update { currentValue -> val newState = controller.advanceToNextStep(state = currentValue.tripState) handleStateUpdate(newState, location) @@ -273,7 +294,7 @@ class FerrostarCore( locationProvider.removeListener(this) _navigationController?.destroy() _navigationController = null - _state = null + _state.value = NavigationState() _queuedUtteranceIds.clear() } diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index 3cbd2c26..b243fe86 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -2,6 +2,9 @@ package com.stadiamaps.ferrostar.core import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.stadiamaps.ferrostar.core.extensions.deviation +import com.stadiamaps.ferrostar.core.extensions.progress +import com.stadiamaps.ferrostar.core.extensions.visualInstruction import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @@ -23,21 +26,48 @@ data class NavigationUiState( val progress: TripProgress?, val isCalculatingNewRoute: Boolean?, val routeDeviation: RouteDeviation? -) +) { + companion object { + fun fromFerrostar(coreState: NavigationState, userLocation: UserLocation): NavigationUiState = + NavigationUiState( + snappedLocation = userLocation, + // TODO: Heading/course over ground + heading = null, + routeGeometry = coreState.routeGeometry, + visualInstruction = coreState.tripState.visualInstruction(), + spokenInstruction = null, + progress = coreState.tripState.progress(), + isCalculatingNewRoute = coreState.isCalculatingNewRoute, + routeDeviation = coreState.tripState.deviation()) + } +} + +interface NavigationViewModel { + val uiState: StateFlow +} + +class DefaultNavigationViewModel( + private val ferrostarCore: FerrostarCore, + locationProvider: LocationProvider +) : ViewModel(), NavigationViewModel { + + private var lastLocation: UserLocation -class NavigationViewModel( - stateFlow: StateFlow, - initialUserLocation: UserLocation, -) : ViewModel() { - private var lastLocation: UserLocation = initialUserLocation + init { + lastLocation = + requireNotNull(locationProvider.lastLocation) { + "LocationProvider must have a last location." + } + } - val uiState = - stateFlow + override val uiState = + ferrostarCore.state .map { coreState -> lastLocation = when (coreState.tripState) { is TripState.Navigating -> coreState.tripState.snappedUserLocation - is TripState.Complete -> lastLocation + is TripState.Complete, + TripState.Idle -> lastLocation } uiState(coreState, lastLocation) @@ -48,39 +78,12 @@ class NavigationViewModel( .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(), - initialValue = uiState(stateFlow.value, initialUserLocation)) + initialValue = uiState(ferrostarCore.state.value, lastLocation)) + + fun stopNavigation() { + ferrostarCore.stopNavigation() + } private fun uiState(coreState: NavigationState, location: UserLocation) = - NavigationUiState( - snappedLocation = location, - // TODO: Heading/course over ground - heading = null, - routeGeometry = coreState.routeGeometry, - visualInstruction = visualInstructionForState(coreState.tripState), - spokenInstruction = null, - progress = progressForState(coreState.tripState), - isCalculatingNewRoute = coreState.isCalculatingNewRoute, - routeDeviation = deviationForState(coreState.tripState)) + NavigationUiState.fromFerrostar(coreState, location) } - -private fun progressForState(newState: TripState) = - when (newState) { - is TripState.Navigating -> newState.progress - is TripState.Complete -> null - } - -private fun visualInstructionForState(newState: TripState) = - try { - when (newState) { - is TripState.Navigating -> newState.visualInstruction - is TripState.Complete -> null - } - } catch (_: NoSuchElementException) { - null - } - -private fun deviationForState(newState: TripState) = - when (newState) { - is TripState.Navigating -> newState.deviation - is TripState.Complete -> null - } diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/TripStateExtensions.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/TripStateExtensions.kt new file mode 100644 index 00000000..03b2b5ac --- /dev/null +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/TripStateExtensions.kt @@ -0,0 +1,43 @@ +package com.stadiamaps.ferrostar.core.extensions + +import uniffi.ferrostar.TripState + +/** + * Get the progress of the trip while navigating. + * + * @return The progress of the trip, or null if the trip is not navigating. + */ +fun TripState.progress() = + when (this) { + is TripState.Navigating -> this.progress + is TripState.Complete, + TripState.Idle -> null + } + +/** + * Get the visual instruction for the current step. + * + * @return The visual instruction for the current step or null. + */ +fun TripState.visualInstruction() = + try { + when (this) { + is TripState.Navigating -> this.visualInstruction + is TripState.Complete, + TripState.Idle -> null + } + } catch (_: NoSuchElementException) { + null + } + +/** + * Get the deviation handler from the trip. + * + * @return The deviation handler if navigating or null. + */ +fun TripState.deviation() = + when (this) { + is TripState.Navigating -> this.deviation + is TripState.Complete, + TripState.Idle -> null + } diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/mock/MockNavigationState.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/mock/MockNavigationState.kt index 824708d0..62b9bda6 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/mock/MockNavigationState.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/mock/MockNavigationState.kt @@ -1,7 +1,11 @@ package com.stadiamaps.ferrostar.core.mock +import androidx.lifecycle.ViewModel import com.stadiamaps.ferrostar.core.NavigationState +import com.stadiamaps.ferrostar.core.NavigationUiState +import com.stadiamaps.ferrostar.core.NavigationViewModel import java.time.Instant +import kotlinx.coroutines.flow.StateFlow import uniffi.ferrostar.CourseOverGround import uniffi.ferrostar.GeographicCoordinate import uniffi.ferrostar.ManeuverModifier @@ -53,3 +57,9 @@ fun NavigationState.Companion.pedestrianExample(): NavigationState { routeGeometry = listOf(), isCalculatingNewRoute = false) } + +fun NavigationUiState.Companion.pedestrianExample(): NavigationUiState = + fromFerrostar(NavigationState.pedestrianExample(), UserLocation.pedestrianExample()) + +class MockNavigationViewModel(override val uiState: StateFlow) : + ViewModel(), NavigationViewModel diff --git a/android/demo-app/build.gradle b/android/demo-app/build.gradle index 73732f5a..e49a2483 100644 --- a/android/demo-app/build.gradle +++ b/android/demo-app/build.gradle @@ -29,7 +29,7 @@ android { } } compileOptions { - coreLibraryDesugaringEnabled true +// coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -48,7 +48,7 @@ android { dependencies { // Temporary until we can drop support for API < 26 - coreLibraryDesugaring libs.desugar.jdk.libs +// coreLibraryDesugaring libs.desugar.jdk.libs implementation libs.androidx.ktx implementation libs.androidx.lifecycle.runtime.ktx diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt index 1d53ba42..587f339f 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt @@ -46,6 +46,11 @@ object AppModule { profile = "bicycle", httpClient = httpClient, locationProvider = locationProvider, + navigationControllerConfig = + NavigationControllerConfig( + StepAdvanceMode.RelativeLineStringDistance( + minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U), + RouteDeviationTracking.StaticThreshold(25U, 10.0)), costingOptions = mapOf("bicycle" to mapOf("use_roads" to 0.2))) // Not all navigation apps will require this sort of extra configuration. diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt index 347118f9..8dd58e81 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt @@ -29,9 +29,6 @@ import com.stadiamaps.ferrostar.support.initialSimulatedLocation import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import uniffi.ferrostar.GeographicCoordinate -import uniffi.ferrostar.NavigationControllerConfig -import uniffi.ferrostar.RouteDeviationTracking -import uniffi.ferrostar.StepAdvanceMode import uniffi.ferrostar.Waypoint import uniffi.ferrostar.WaypointKind @@ -81,15 +78,7 @@ fun DemoNavigationScene( )) val route = routes.first() - viewModel = - core.startNavigation( - route = route, - config = - NavigationControllerConfig( - StepAdvanceMode.RelativeLineStringDistance( - minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U), - RouteDeviationTracking.StaticThreshold(25U, 10.0)), - ) + viewModel = core.startNavigation(route = route) locationProvider.setSimulatedRoute(route) } diff --git a/android/maplibreui/build.gradle b/android/maplibreui/build.gradle index 7707412c..d2ce271a 100644 --- a/android/maplibreui/build.gradle +++ b/android/maplibreui/build.gradle @@ -10,7 +10,7 @@ android { compileSdk 34 defaultConfig { - minSdk 25 + minSdk 26 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -23,7 +23,7 @@ android { } } compileOptions { - coreLibraryDesugaringEnabled true +// coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -43,7 +43,7 @@ android { dependencies { // For as long as we support API 25; once we can raise support to 26, all is fine - coreLibraryDesugaring libs.desugar.jdk.libs +// coreLibraryDesugaring libs.desugar.jdk.libs implementation libs.androidx.ktx implementation platform(libs.kotlin.bom) diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/LandscapeNavigationView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/LandscapeNavigationView.kt index 81cdfff5..4a7c9cba 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/LandscapeNavigationView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/LandscapeNavigationView.kt @@ -31,15 +31,15 @@ import com.maplibre.compose.settings.MarginInsets import com.stadiamaps.ferrostar.composeui.views.ArrivalView import com.stadiamaps.ferrostar.composeui.views.InstructionsView import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridView -import com.stadiamaps.ferrostar.core.NavigationState import com.stadiamaps.ferrostar.core.NavigationUiState import com.stadiamaps.ferrostar.core.NavigationViewModel +import com.stadiamaps.ferrostar.core.mock.MockNavigationViewModel import com.stadiamaps.ferrostar.core.mock.pedestrianExample import com.stadiamaps.ferrostar.maplibreui.NavigationMapView import com.stadiamaps.ferrostar.maplibreui.extensions.NavigationDefault import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import kotlinx.coroutines.flow.MutableStateFlow -import uniffi.ferrostar.UserLocation +import kotlinx.coroutines.flow.asStateFlow /** * A portrait orientation of the navigation view with instructions, default controls and the @@ -117,9 +117,8 @@ fun LandscapeNavigationView( @Composable private fun LandscapeNavigationViewPreview() { val viewModel = - NavigationViewModel( - MutableStateFlow(NavigationState.pedestrianExample()), - initialUserLocation = UserLocation.pedestrianExample()) + MockNavigationViewModel( + MutableStateFlow(NavigationUiState.pedestrianExample()).asStateFlow()) LandscapeNavigationView( Modifier.fillMaxSize(), "https://demotiles.maplibre.org/style.json", viewModel = viewModel) diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/PortraitNavigationView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/PortraitNavigationView.kt index ec07056c..283ad1a3 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/PortraitNavigationView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/PortraitNavigationView.kt @@ -27,15 +27,15 @@ import com.maplibre.compose.settings.MarginInsets import com.stadiamaps.ferrostar.composeui.views.ArrivalView import com.stadiamaps.ferrostar.composeui.views.InstructionsView import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridView -import com.stadiamaps.ferrostar.core.NavigationState import com.stadiamaps.ferrostar.core.NavigationUiState import com.stadiamaps.ferrostar.core.NavigationViewModel +import com.stadiamaps.ferrostar.core.mock.MockNavigationViewModel import com.stadiamaps.ferrostar.core.mock.pedestrianExample import com.stadiamaps.ferrostar.maplibreui.NavigationMapView import com.stadiamaps.ferrostar.maplibreui.extensions.NavigationDefault import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import kotlinx.coroutines.flow.MutableStateFlow -import uniffi.ferrostar.UserLocation +import kotlinx.coroutines.flow.asStateFlow /** * A portrait orientation of the navigation view with instructions, default controls and the @@ -103,9 +103,8 @@ fun PortraitNavigationView( @Composable private fun PortraitNavigationViewPreview() { val viewModel = - NavigationViewModel( - MutableStateFlow(NavigationState.pedestrianExample()), - initialUserLocation = UserLocation.pedestrianExample()) + MockNavigationViewModel( + MutableStateFlow(NavigationUiState.pedestrianExample()).asStateFlow()) PortraitNavigationView( Modifier.fillMaxSize(), "https://demotiles.maplibre.org/style.json", viewModel = viewModel) diff --git a/apple/Sources/FerrostarCore/FerrostarCore.swift b/apple/Sources/FerrostarCore/FerrostarCore.swift index 6abe6bb8..3094f02e 100644 --- a/apple/Sources/FerrostarCore/FerrostarCore.swift +++ b/apple/Sources/FerrostarCore/FerrostarCore.swift @@ -85,24 +85,25 @@ public protocol FerrostarCoreDelegate: AnyObject { private let routeProvider: RouteProvider private let locationProvider: LocationProviding private var navigationController: NavigationControllerProtocol? - private var tripState: TripState? private var routeRequestInFlight = false private var lastAutomaticRecalculation: Date? = nil private var lastLocation: UserLocation? = nil private var recalculationTask: Task? private var queuedUtteranceIDs: Set = Set() - private var config: SwiftNavigationControllerConfig? + private var config: SwiftNavigationControllerConfig public init( routeProvider: RouteProvider, locationProvider: LocationProviding, + navigationControllerConfig: SwiftNavigationControllerConfig, networkSession: URLRequestLoading ) { self.routeProvider = routeProvider self.locationProvider = locationProvider + self.config = navigationControllerConfig self.networkSession = networkSession - + super.init() // Location provider setup @@ -113,6 +114,7 @@ public protocol FerrostarCoreDelegate: AnyObject { valhallaEndpointUrl: URL, profile: String, locationProvider: LocationProviding, + navigationControllerConfig: SwiftNavigationControllerConfig, costingOptions: [String: Any] = [:], networkSession: URLRequestLoading = URLSession.shared ) throws { @@ -131,6 +133,7 @@ public protocol FerrostarCoreDelegate: AnyObject { self.init( routeProvider: .routeAdapter(adapter), locationProvider: locationProvider, + navigationControllerConfig: navigationControllerConfig, networkSession: networkSession ) } @@ -138,11 +141,13 @@ public protocol FerrostarCoreDelegate: AnyObject { public convenience init( routeAdapter: RouteAdapterProtocol, locationProvider: LocationProviding, + navigationControllerConfig: SwiftNavigationControllerConfig, networkSession: URLRequestLoading = URLSession.shared ) { self.init( routeProvider: .routeAdapter(routeAdapter), locationProvider: locationProvider, + navigationControllerConfig: navigationControllerConfig, networkSession: networkSession ) } @@ -150,11 +155,13 @@ public protocol FerrostarCoreDelegate: AnyObject { public convenience init( customRouteProvider: CustomRouteProvider, locationProvider: LocationProviding, + navigationControllerConfig: SwiftNavigationControllerConfig, networkSession: URLRequestLoading = URLSession.shared ) { self.init( routeProvider: .customProvider(customRouteProvider), locationProvider: locationProvider, + navigationControllerConfig: navigationControllerConfig, networkSession: networkSession ) } @@ -206,7 +213,11 @@ public protocol FerrostarCoreDelegate: AnyObject { } /// Starts navigation with the given route. Any previous navigation session is dropped. - public func startNavigation(route: Route, config: SwiftNavigationControllerConfig) throws { + /// + /// - Parameters: + /// - route: The route to navigate. + /// - config: Override the configuration for the navigation session. This was provided on init. + public func startNavigation(route: Route, config: SwiftNavigationControllerConfig? = nil) throws { // This is technically possible, so we need to check and throw, but // it should be rather difficult to get a location fix, get a route, // and then somehow this property go nil again. @@ -215,25 +226,27 @@ public protocol FerrostarCoreDelegate: AnyObject { } // TODO: We should be able to circumvent this and simply start updating, wait and start nav. - self.config = config - + // Apply the new config if one was provided to override. + self.config = config ?? self.config + + // Configure the navigation controller. This is required to build the initial state. + let controller = NavigationController(route: route, config: self.config.ffiValue) + navigationController = controller + locationProvider.startUpdating() state = NavigationState( - snappedLocation: location, - heading: locationProvider.lastHeading, - fullRouteShape: route.geometry, - steps: route.steps + tripState: controller.getInitialState(location: location), + routeGeometry: route.geometry ) - let controller = NavigationController(route: route, config: config.ffiValue) - navigationController = controller + DispatchQueue.main.async { self.update(newState: controller.getInitialState(location: location), location: location) } } public func advanceToNextStep() { - guard let controller = navigationController, let tripState, let lastLocation else { + guard let controller = navigationController, let tripState = state?.tripState, let lastLocation else { return } @@ -247,7 +260,6 @@ public protocol FerrostarCoreDelegate: AnyObject { public func stopNavigation() { navigationController = nil state = nil - tripState = nil queuedUtteranceIDs.removeAll() locationProvider.stopUpdating() } @@ -257,25 +269,18 @@ public protocol FerrostarCoreDelegate: AnyObject { /// You should call this rather than setting properties directly private func update(newState: TripState, location: UserLocation) { DispatchQueue.main.async { - self.tripState = newState + self.state?.tripState = newState switch newState { case let .navigating( - snappedUserLocation: snappedLocation, - remainingSteps: remainingSteps, + snappedUserLocation: _, + remainingSteps: _, remainingWaypoints: remainingWaypoints, - progress: progress, + progress: _, deviation: deviation, - visualInstruction: visualInstruction, + visualInstruction: _, spokenInstruction: spokenInstruction ): - self.state?.snappedLocation = snappedLocation - self.state?.currentStep = remainingSteps.first - self.state?.visualInstruction = visualInstruction - self.state?.spokenInstruction = spokenInstruction - self.state?.progress = progress - - self.state?.routeDeviation = deviation switch deviation { case .noDeviation: // No action @@ -306,10 +311,10 @@ public protocol FerrostarCoreDelegate: AnyObject { ) if let delegate = self.delegate { delegate.core(self, loadedAlternateRoutes: routes) - } else if let route = routes.first, let config = self.config { + } else if let route = routes.first { // Default behavior when no delegate is assigned: // accept the first route, as this is what most users want when they go off route. - try self.startNavigation(route: route, config: config) + try self.startNavigation(route: route, config: self.config) } } catch { // Do nothing; this exists to enable us to run what amounts to an "async defer" @@ -333,12 +338,8 @@ public protocol FerrostarCoreDelegate: AnyObject { self.spokenInstructionObserver?.spokenInstructionTriggered(spokenInstruction) } } - case .complete: - // TODO: "You have arrived"? - self.state?.visualInstruction = nil - self.state?.snappedLocation = location - self.state?.spokenInstruction = nil - self.state?.routeDeviation = nil + default: + break } } } @@ -348,7 +349,7 @@ extension FerrostarCore: LocationManagingDelegate { @MainActor public func locationManager(_: LocationProviding, didUpdateLocations locations: [UserLocation]) { guard let location = locations.last, - let state = tripState, + let state = state?.tripState, let newState = navigationController?.updateUserLocation(location: location, state: state) else { return @@ -360,7 +361,8 @@ extension FerrostarCore: LocationManagingDelegate { } public func locationManager(_: LocationProviding, didUpdateHeading newHeading: Heading) { - state?.heading = newHeading + // TODO: Make use of heading in TripState? +// state?.heading = newHeading } public func locationManager(_: LocationProviding, didFailWithError _: Error) { diff --git a/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift b/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift index 857c6c34..30573141 100644 --- a/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift +++ b/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift @@ -4,31 +4,8 @@ import Foundation public extension NavigationState { static let pedestrianExample = NavigationState( - snappedLocation: UserLocation( - latitude: samplePedestrianWaypoints.first!.lat, - longitude: samplePedestrianWaypoints.first!.lng, - horizontalAccuracy: 0, - course: 0, - courseAccuracy: 0, - timestamp: Date(), - speed: 0, - speedAccuracy: 0 - ), - fullRouteShape: samplePedestrianWaypoints, - steps: [], - progress: TripProgress( - distanceToNextManeuver: 0, - distanceRemaining: 0, - durationRemaining: 0 - ) - ) - - static func modifiedPedestrianExample(droppingNWaypoints n: Int) -> NavigationState { - let remainingLocations = Array(samplePedestrianWaypoints.dropFirst(n)) - let lastUserLocation = remainingLocations.first! - - var result = NavigationState( - snappedLocation: UserLocation( + tripState: .navigating( + snappedUserLocation: UserLocation( latitude: samplePedestrianWaypoints.first!.lat, longitude: samplePedestrianWaypoints.first!.lng, horizontalAccuracy: 0, @@ -38,40 +15,79 @@ public extension NavigationState { speed: 0, speedAccuracy: 0 ), - fullRouteShape: samplePedestrianWaypoints, - steps: [RouteStep( - geometry: [lastUserLocation], - distance: 100, - duration: 99, - roadName: "Jefferson St.", - instruction: "Walk west on Jefferson St.", - visualInstructions: [ - VisualInstruction( - primaryContent: VisualInstructionContent( - text: "Hyde Street", - maneuverType: .turn, - maneuverModifier: .left, - roundaboutExitDegrees: nil - ), - secondaryContent: nil, triggerDistanceBeforeManeuver: 42.0 - ), - ], - spokenInstructions: [] - )], + remainingSteps: [], + remainingWaypoints: [], progress: TripProgress( - distanceToNextManeuver: 5, - distanceRemaining: 100, - durationRemaining: 99 - ) - ) + distanceToNextManeuver: 0, + distanceRemaining: 0, + durationRemaining: 0 + ), + deviation: .noDeviation, + visualInstruction: nil, + spokenInstruction: nil + ), + routeGeometry: samplePedestrianWaypoints, + isCalculatingNewRoute: false + ) + + static func modifiedPedestrianExample(droppingNWaypoints n: Int) -> NavigationState { + let remainingLocations = Array(samplePedestrianWaypoints.dropFirst(n)) + let lastUserLocation = remainingLocations.first! - result.snappedLocation = UserLocation( - coordinates: samplePedestrianWaypoints.first!, - horizontalAccuracy: 10, - courseOverGround: CourseOverGround(degrees: 0, accuracy: 10), - timestamp: Date(), - speed: Speed(value: 0, accuracy: 2) + var result = NavigationState( + tripState: .navigating( + snappedUserLocation: UserLocation( + latitude: samplePedestrianWaypoints.first!.lat, + longitude: samplePedestrianWaypoints.first!.lng, + horizontalAccuracy: 0, + course: 0, + courseAccuracy: 0, + timestamp: Date(), + speed: 0, + speedAccuracy: 0 + ), + remainingSteps: [ + RouteStep( + geometry: [lastUserLocation], + distance: 100, + duration: 99, + roadName: "Jefferson St.", + instruction: "Walk west on Jefferson St.", + visualInstructions: [ + VisualInstruction( + primaryContent: VisualInstructionContent( + text: "Hyde Street", + maneuverType: .turn, + maneuverModifier: .left, + roundaboutExitDegrees: nil + ), + secondaryContent: nil, triggerDistanceBeforeManeuver: 42.0 + ), + ], + spokenInstructions: [] + ) + ], + remainingWaypoints: [], + progress: TripProgress( + distanceToNextManeuver: 5, + distanceRemaining: 100, + durationRemaining: 99 + ), + deviation: .noDeviation, + visualInstruction: nil, + spokenInstruction: nil), + routeGeometry: samplePedestrianWaypoints, + isCalculatingNewRoute: false ) + + // TODO: Move this to the NavigationState? +// result.snappedLocation = UserLocation( +// coordinates: samplePedestrianWaypoints.first!, +// horizontalAccuracy: 10, +// courseOverGround: CourseOverGround(degrees: 0, accuracy: 10), +// timestamp: Date(), +// speed: Speed(value: 0, accuracy: 2) +// ) return result } diff --git a/apple/Sources/FerrostarCore/NavigationState.swift b/apple/Sources/FerrostarCore/NavigationState.swift index 7e5681a2..c44f1d55 100644 --- a/apple/Sources/FerrostarCore/NavigationState.swift +++ b/apple/Sources/FerrostarCore/NavigationState.swift @@ -7,31 +7,48 @@ import Foundation /// While the core generally does not include UI, this is purely at the model layer and should be implemented /// the same for all frontends. public struct NavigationState: Hashable { - public internal(set) var snappedLocation: UserLocation - public internal(set) var heading: Heading? - public internal(set) var fullRouteShape: [GeographicCoordinate] - public internal(set) var currentStep: RouteStep? - public internal(set) var visualInstruction: VisualInstruction? + + public internal(set) var tripState: TripState + public internal(set) var routeGeometry: [GeographicCoordinate] + // TODO: This probably gets removed once we have an observer protocol - public internal(set) var spokenInstruction: SpokenInstruction? - public internal(set) var progress: TripProgress? + /// Indicates when the core is calculating a new route due to the user being off route public internal(set) var isCalculatingNewRoute: Bool = false - public internal(set) var routeDeviation: RouteDeviation? - - init( - snappedLocation: UserLocation, - heading: Heading? = nil, - fullRouteShape: [GeographicCoordinate], - steps: [RouteStep], - progress: TripProgress? = nil - ) { - self.snappedLocation = snappedLocation - self.heading = heading - self.fullRouteShape = fullRouteShape - self.progress = progress - currentStep = steps.first - visualInstruction = currentStep?.visualInstructions.first - spokenInstruction = currentStep?.spokenInstructions.first + + init(tripState: TripState, routeGeometry: [GeographicCoordinate], isCalculatingNewRoute: Bool = false) { + self.tripState = tripState + self.routeGeometry = routeGeometry + self.isCalculatingNewRoute = isCalculatingNewRoute } + + // TODO: Delete once using tripState +// public internal(set) var snappedLocation: UserLocation +// public internal(set) var routeDeviation: RouteDeviation? +// public internal(set) var spokenInstruction: SpokenInstruction? +// public internal(set) var visualInstruction: VisualInstruction? +// public internal(set) var progress: TripProgress? + + // TODO: This may be part of TripState? +// public internal(set) var heading: Heading? +// public internal(set) var currentStep: RouteStep? + + // TODO: Delete once using routeGeometry +// public internal(set) var fullRouteShape: [GeographicCoordinate] + +// init( +// snappedLocation: UserLocation, +// heading: Heading? = nil, +// fullRouteShape: [GeographicCoordinate], +// steps: [RouteStep], +// progress: TripProgress? = nil +// ) { +// self.snappedLocation = snappedLocation +// self.heading = heading +// self.fullRouteShape = fullRouteShape +// self.progress = progress +// currentStep = steps.first +// visualInstruction = currentStep?.visualInstructions.first +// spokenInstruction = currentStep?.spokenInstructions.first +// } } diff --git a/apple/Sources/FerrostarMapLibreUI/MapLibre Extensions.swift b/apple/Sources/FerrostarMapLibreUI/MapLibre Extensions.swift index 9fbcb9b9..5f0e9b63 100644 --- a/apple/Sources/FerrostarMapLibreUI/MapLibre Extensions.swift +++ b/apple/Sources/FerrostarMapLibreUI/MapLibre Extensions.swift @@ -4,11 +4,11 @@ import MapLibre extension NavigationState { var routePolyline: MLNPolyline { - MLNPolylineFeature(coordinates: fullRouteShape.map(\.clLocationCoordinate2D)) + MLNPolylineFeature(coordinates: routeGeometry.map(\.clLocationCoordinate2D)) } var remainingRoutePolyline: MLNPolyline { // FIXME: - MLNPolylineFeature(coordinates: fullRouteShape.map(\.clLocationCoordinate2D)) + MLNPolylineFeature(coordinates: routeGeometry.map(\.clLocationCoordinate2D)) } } diff --git a/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift index 6bc7673a..33e137f3 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift @@ -134,9 +134,13 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn formatter.locale = Locale(identifier: "en-US") formatter.units = .imperial + guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { + return EmptyView() + } + return DynamicallyOrientingNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, - camera: .constant(.center(state.snappedLocation.clLocation.coordinate, zoom: 12)), + camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), navigationState: state ) .navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter)) @@ -149,9 +153,13 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn formatter.locale = Locale(identifier: "en-US") formatter.units = .metric + guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { + return EmptyView() + } + return DynamicallyOrientingNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, - camera: .constant(.center(state.snappedLocation.clLocation.coordinate, zoom: 12)), + camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), navigationState: state ) .navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter)) diff --git a/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift index b03395a1..295ab2f0 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift @@ -103,9 +103,13 @@ public struct LandscapeNavigationView: View { formatter.locale = Locale(identifier: "en-US") formatter.units = .imperial + guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { + return EmptyView() + } + return LandscapeNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, - camera: .constant(.center(state.snappedLocation.clLocation.coordinate, zoom: 12)), + camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), navigationState: state ) .navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter)) @@ -120,9 +124,13 @@ public struct LandscapeNavigationView: View { formatter.locale = Locale(identifier: "en-US") formatter.units = .metric + guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { + return EmptyView() + } + return LandscapeNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, - camera: .constant(.center(state.snappedLocation.clLocation.coordinate, zoom: 12)), + camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), navigationState: state ) .navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter)) diff --git a/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift b/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift index 84207a7b..2bd4753a 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift @@ -83,13 +83,13 @@ public struct NavigationMapView: View { } private func updateCameraIfNeeded() { - if let snappedLocation = navigationState?.snappedLocation, + if case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = navigationState?.tripState, // There is no reason to push an update if the coordinate and heading are the same. // That's all that gets displayed, so it's all that MapLibre should care about. - locationManager.lastLocation.coordinate != snappedLocation.coordinates - .clLocationCoordinate2D || locationManager.lastLocation.course != snappedLocation.clLocation.course + locationManager.lastLocation.coordinate != userLocation.coordinates + .clLocationCoordinate2D || locationManager.lastLocation.course != userLocation.clLocation.course { - locationManager.lastLocation = snappedLocation.clLocation + locationManager.lastLocation = userLocation.clLocation } } } @@ -98,9 +98,13 @@ public struct NavigationMapView: View { // TODO: Make map URL configurable but gitignored let state = NavigationState.modifiedPedestrianExample(droppingNWaypoints: 4) + guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { + return EmptyView() + } + return NavigationMapView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, - camera: .constant(.center(state.snappedLocation.clLocation.coordinate, zoom: 12)), + camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), navigationState: state, onStyleLoaded: { _ in } ) diff --git a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift index 29a4292b..a34318c3 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift @@ -47,20 +47,20 @@ struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView var body: some View { HStack { VStack { - if let navigationState, - let visualInstructions = navigationState.visualInstruction + if case let .navigating(_, _, _, progress: progress, _, visualInstruction: visualInstruction, _) = navigationState?.tripState, + let visualInstruction { InstructionsView( - visualInstruction: visualInstructions, + visualInstruction: visualInstruction, distanceFormatter: formatterCollection.distanceFormatter, - distanceToNextManeuver: navigationState.progress?.distanceToNextManeuver + distanceToNextManeuver: progress.distanceToNextManeuver ) .padding(.top, 16) } Spacer() - if let progress = navigationState?.progress { + if case let .navigating(_, _, _, progress: progress, _, _, _) = navigationState?.tripState { ArrivalView( progress: progress, onTapExit: onTapExit diff --git a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift index 8bb52d6d..53abafe7 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift @@ -46,13 +46,13 @@ struct PortraitNavigationOverlayView: View, CustomizableNavigatingInnerGridView var body: some View { VStack { - if let navigationState, - let visualInstructions = navigationState.visualInstruction + if case let .navigating(_, _, _, progress: progress, _, visualInstruction: visualInstruction, _) = navigationState?.tripState, + let visualInstruction { InstructionsView( - visualInstruction: visualInstructions, + visualInstruction: visualInstruction, distanceFormatter: formatterCollection.distanceFormatter, - distanceToNextManeuver: navigationState.progress?.distanceToNextManeuver + distanceToNextManeuver: progress.distanceToNextManeuver ) .padding(.horizontal, 16) } @@ -80,7 +80,7 @@ struct PortraitNavigationOverlayView: View, CustomizableNavigatingInnerGridView } .padding(.horizontal, 16) - if let progress = navigationState?.progress { + if case let .navigating(_, _, _, progress: progress, _, _, _) = navigationState?.tripState { ArrivalView( progress: progress, onTapExit: onTapExit diff --git a/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift index 96192399..19f42636 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift @@ -104,9 +104,13 @@ public struct PortraitNavigationView: View, CustomizableNavigatingInnerGridView formatter.locale = Locale(identifier: "en-US") formatter.units = .imperial + guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { + return EmptyView() + } + return PortraitNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, - camera: .constant(.center(state.snappedLocation.clLocation.coordinate, zoom: 12)), + camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), navigationState: state ) .navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter)) @@ -120,9 +124,13 @@ public struct PortraitNavigationView: View, CustomizableNavigatingInnerGridView formatter.locale = Locale(identifier: "en-US") formatter.units = .metric + guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { + return EmptyView() + } + return PortraitNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, - camera: .constant(.center(state.snappedLocation.clLocation.coordinate, zoom: 12)), + camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), navigationState: state ) .navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter)) diff --git a/apple/Sources/UniFFI/ferrostar.swift b/apple/Sources/UniFFI/ferrostar.swift index 2fabd99e..34bf9801 100644 --- a/apple/Sources/UniFFI/ferrostar.swift +++ b/apple/Sources/UniFFI/ferrostar.swift @@ -3288,6 +3288,10 @@ extension StepAdvanceMode: Equatable, Hashable {} */ public enum TripState { + /** + * The navigation controller is idle + */ + case idle case navigating(snappedUserLocation: UserLocation, /** * The ordered list of steps that remain in the trip. @@ -3320,6 +3324,9 @@ public enum TripState { * * Note it is the responsibility of the platform layer to ensure that utterances are not synthesized multiple times. This property simply reports the current spoken instruction. */ spokenInstruction: SpokenInstruction?) + /** + * The navigation controller has reached the end of the route. + */ case complete } @@ -3329,7 +3336,9 @@ public struct FfiConverterTypeTripState: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TripState { let variant: Int32 = try readInt(&buf) switch variant { - case 1: return try .navigating( + case 1: return .idle + + case 2: return try .navigating( snappedUserLocation: FfiConverterTypeUserLocation.read(from: &buf), remainingSteps: FfiConverterSequenceTypeRouteStep.read(from: &buf), remainingWaypoints: FfiConverterSequenceTypeWaypoint.read(from: &buf), @@ -3339,7 +3348,7 @@ public struct FfiConverterTypeTripState: FfiConverterRustBuffer { spokenInstruction: FfiConverterOptionTypeSpokenInstruction.read(from: &buf) ) - case 2: return .complete + case 3: return .complete default: throw UniffiInternalError.unexpectedEnumCase } @@ -3347,6 +3356,9 @@ public struct FfiConverterTypeTripState: FfiConverterRustBuffer { public static func write(_ value: TripState, into buf: inout [UInt8]) { switch value { + case .idle: + writeInt(&buf, Int32(1)) + case let .navigating( snappedUserLocation, remainingSteps, @@ -3356,7 +3368,7 @@ public struct FfiConverterTypeTripState: FfiConverterRustBuffer { visualInstruction, spokenInstruction ): - writeInt(&buf, Int32(1)) + writeInt(&buf, Int32(2)) FfiConverterTypeUserLocation.write(snappedUserLocation, into: &buf) FfiConverterSequenceTypeRouteStep.write(remainingSteps, into: &buf) FfiConverterSequenceTypeWaypoint.write(remainingWaypoints, into: &buf) @@ -3366,7 +3378,7 @@ public struct FfiConverterTypeTripState: FfiConverterRustBuffer { FfiConverterOptionTypeSpokenInstruction.write(spokenInstruction, into: &buf) case .complete: - writeInt(&buf, Int32(2)) + writeInt(&buf, Int32(3)) } } } diff --git a/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift b/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift index 54d9437f..353f7183 100644 --- a/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift +++ b/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift @@ -99,7 +99,8 @@ final class FerrostarCoreTests: XCTestCase { let core = FerrostarCore( routeAdapter: routeAdapter, - locationProvider: SimulatedLocationProvider(), + locationProvider: SimulatedLocationProvider(), + navigationControllerConfig: .init(stepAdvance: .manual, routeDeviationTracking: .none), networkSession: mockSession ) @@ -133,6 +134,7 @@ final class FerrostarCoreTests: XCTestCase { let core = FerrostarCore( routeAdapter: mockRouteAdapter, locationProvider: SimulatedLocationProvider(), + navigationControllerConfig: .init(stepAdvance: .manual, routeDeviationTracking: .none), networkSession: mockSession ) @@ -165,6 +167,7 @@ final class FerrostarCoreTests: XCTestCase { valhallaEndpointUrl: valhallaEndpointUrl, profile: "low_speed_vehicle", locationProvider: SimulatedLocationProvider(), + navigationControllerConfig: .init(stepAdvance: .manual, routeDeviationTracking: .none), costingOptions: ["low_speed_vehicle": ["vehicle_type": "golf_cart"]], networkSession: mockSession ) @@ -191,7 +194,8 @@ final class FerrostarCoreTests: XCTestCase { let core = FerrostarCore( customRouteProvider: mockCustomRouteProvider, - locationProvider: SimulatedLocationProvider(), + locationProvider: SimulatedLocationProvider(), + navigationControllerConfig: .init(stepAdvance: .manual, routeDeviationTracking: .none), networkSession: mockSession ) @@ -227,7 +231,8 @@ final class FerrostarCoreTests: XCTestCase { let core = FerrostarCore( routeAdapter: mockRouteAdapter, - locationProvider: locationProvider, + locationProvider: locationProvider, + navigationControllerConfig: .init(stepAdvance: .manual, routeDeviationTracking: .none), networkSession: mockSession ) diff --git a/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift b/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift index 29011779..7611e104 100644 --- a/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift +++ b/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift @@ -17,7 +17,8 @@ final class ValhallaCoreTests: XCTestCase { let core = try FerrostarCore( valhallaEndpointUrl: valhallaEndpointUrl, profile: "auto", - locationProvider: SimulatedLocationProvider(), + locationProvider: SimulatedLocationProvider(), + navigationControllerConfig: .init(stepAdvance: .manual, routeDeviationTracking: .none), networkSession: mockSession ) let routes = try await core.getRoutes( diff --git a/common/ferrostar/src/navigation_controller/mod.rs b/common/ferrostar/src/navigation_controller/mod.rs index cfe7ca41..6daa03a7 100644 --- a/common/ferrostar/src/navigation_controller/mod.rs +++ b/common/ferrostar/src/navigation_controller/mod.rs @@ -85,6 +85,7 @@ impl NavigationController { /// urban tunnel). We leave this decision to the app developer and provide this as a convenience. pub fn advance_to_next_step(&self, state: &TripState) -> TripState { match state { + TripState::Idle => TripState::Idle, TripState::Navigating { snapped_user_location, ref remaining_steps, @@ -161,6 +162,7 @@ impl NavigationController { /// Updates the user's current location and updates the navigation state accordingly. pub fn update_user_location(&self, location: UserLocation, state: &TripState) -> TripState { match state { + TripState::Idle => TripState::Idle, TripState::Navigating { ref remaining_steps, ref remaining_waypoints, @@ -208,6 +210,7 @@ impl NavigationController { // Do not advance intermediate_state } { + TripState::Idle => TripState::Idle, TripState::Navigating { snapped_user_location, remaining_steps, diff --git a/common/ferrostar/src/navigation_controller/models.rs b/common/ferrostar/src/navigation_controller/models.rs index bd9e4a09..9588abab 100644 --- a/common/ferrostar/src/navigation_controller/models.rs +++ b/common/ferrostar/src/navigation_controller/models.rs @@ -29,6 +29,8 @@ pub struct TripProgress { #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[cfg_attr(feature = "wasm-bindgen", derive(Serialize, Deserialize))] pub enum TripState { + /// The navigation controller is idle + Idle, #[cfg_attr(feature = "wasm-bindgen", serde(rename_all = "camelCase"))] Navigating { snapped_user_location: UserLocation, @@ -58,6 +60,7 @@ pub enum TripState { /// Note it is the responsibility of the platform layer to ensure that utterances are not synthesized multiple times. This property simply reports the current spoken instruction. spoken_instruction: Option, }, + /// The navigation controller has reached the end of the route. Complete, } From fb8dc121be0f66b449552cc89b71dcaa1cbc2fb6 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 21:21:19 -0700 Subject: [PATCH 02/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- android/composeui/build.gradle | 4 ++-- android/core/build.gradle | 4 ++-- android/demo-app/build.gradle | 4 ++-- android/maplibreui/build.gradle | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/composeui/build.gradle b/android/composeui/build.gradle index 2db34af8..6cd63ef0 100644 --- a/android/composeui/build.gradle +++ b/android/composeui/build.gradle @@ -24,7 +24,7 @@ android { } } compileOptions { -// coreLibraryDesugaringEnabled true + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -44,7 +44,7 @@ android { dependencies { // For as long as we support API 25; once we can raise support to 26, all is fine -// coreLibraryDesugaring libs.desugar.jdk.libs + coreLibraryDesugaring libs.desugar.jdk.libs implementation platform(libs.kotlin.bom) diff --git a/android/core/build.gradle b/android/core/build.gradle index c8974997..72f67265 100644 --- a/android/core/build.gradle +++ b/android/core/build.gradle @@ -25,7 +25,7 @@ android { } } compileOptions { -// coreLibraryDesugaringEnabled true + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -43,7 +43,7 @@ android { dependencies { // For as long as we support API 25; once we can raise support to 26, all is fine -// coreLibraryDesugaring libs.desugar.jdk.libs + coreLibraryDesugaring libs.desugar.jdk.libs implementation libs.androidx.ktx implementation libs.androidx.appcompat diff --git a/android/demo-app/build.gradle b/android/demo-app/build.gradle index e49a2483..73732f5a 100644 --- a/android/demo-app/build.gradle +++ b/android/demo-app/build.gradle @@ -29,7 +29,7 @@ android { } } compileOptions { -// coreLibraryDesugaringEnabled true + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -48,7 +48,7 @@ android { dependencies { // Temporary until we can drop support for API < 26 -// coreLibraryDesugaring libs.desugar.jdk.libs + coreLibraryDesugaring libs.desugar.jdk.libs implementation libs.androidx.ktx implementation libs.androidx.lifecycle.runtime.ktx diff --git a/android/maplibreui/build.gradle b/android/maplibreui/build.gradle index d2ce271a..bae70409 100644 --- a/android/maplibreui/build.gradle +++ b/android/maplibreui/build.gradle @@ -23,7 +23,7 @@ android { } } compileOptions { -// coreLibraryDesugaringEnabled true + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -43,7 +43,7 @@ android { dependencies { // For as long as we support API 25; once we can raise support to 26, all is fine -// coreLibraryDesugaring libs.desugar.jdk.libs + coreLibraryDesugaring libs.desugar.jdk.libs implementation libs.androidx.ktx implementation platform(libs.kotlin.bom) From 3cc1b4503e07d89cb2f7c141a51d23faea7e2ffc Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 21:21:50 -0700 Subject: [PATCH 03/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 2570780e..06e2af6c 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let binaryTarget: Target let maplibreSwiftUIDSLPackage: Package.Dependency -let useLocalFramework = true +let useLocalFramework = false let useLocalMapLibreSwiftUIDSL = false if useLocalFramework { From 200130974cdde0903330753f2b21c004398875b9 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 21:22:05 -0700 Subject: [PATCH 04/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- apple/Sources/FerrostarCore/FerrostarCore.swift | 12 ++++++------ .../FerrostarCore/Mock/MockNavigationState.swift | 7 ++++--- apple/Sources/FerrostarCore/NavigationState.swift | 15 +++++++-------- .../DynamicallyOrientingNavigationView.swift | 4 ++-- .../Views/LandscapeNavigationView.swift | 4 ++-- .../Views/NavigationMapView.swift | 2 +- .../LandscapeNavigationOverlayView.swift | 5 +++-- .../Overylays/PortraitNavigationOverlayView.swift | 5 +++-- .../Views/PortraitNavigationView.swift | 4 ++-- .../FerrostarCoreTests/FerrostarCoreTests.swift | 6 +++--- .../FerrostarCoreTests/ValhallaCoreTests.swift | 2 +- 11 files changed, 34 insertions(+), 32 deletions(-) diff --git a/apple/Sources/FerrostarCore/FerrostarCore.swift b/apple/Sources/FerrostarCore/FerrostarCore.swift index 3094f02e..f841937d 100644 --- a/apple/Sources/FerrostarCore/FerrostarCore.swift +++ b/apple/Sources/FerrostarCore/FerrostarCore.swift @@ -101,9 +101,9 @@ public protocol FerrostarCoreDelegate: AnyObject { ) { self.routeProvider = routeProvider self.locationProvider = locationProvider - self.config = navigationControllerConfig + config = navigationControllerConfig self.networkSession = networkSession - + super.init() // Location provider setup @@ -228,18 +228,18 @@ public protocol FerrostarCoreDelegate: AnyObject { // Apply the new config if one was provided to override. self.config = config ?? self.config - + // Configure the navigation controller. This is required to build the initial state. let controller = NavigationController(route: route, config: self.config.ffiValue) navigationController = controller - + locationProvider.startUpdating() state = NavigationState( tripState: controller.getInitialState(location: location), routeGeometry: route.geometry ) - + DispatchQueue.main.async { self.update(newState: controller.getInitialState(location: location), location: location) } @@ -360,7 +360,7 @@ extension FerrostarCore: LocationManagingDelegate { update(newState: newState, location: location) } - public func locationManager(_: LocationProviding, didUpdateHeading newHeading: Heading) { + public func locationManager(_: LocationProviding, didUpdateHeading _: Heading) { // TODO: Make use of heading in TripState? // state?.heading = newHeading } diff --git a/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift b/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift index 30573141..2577f228 100644 --- a/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift +++ b/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift @@ -65,7 +65,7 @@ public extension NavigationState { ), ], spokenInstructions: [] - ) + ), ], remainingWaypoints: [], progress: TripProgress( @@ -75,11 +75,12 @@ public extension NavigationState { ), deviation: .noDeviation, visualInstruction: nil, - spokenInstruction: nil), + spokenInstruction: nil + ), routeGeometry: samplePedestrianWaypoints, isCalculatingNewRoute: false ) - + // TODO: Move this to the NavigationState? // result.snappedLocation = UserLocation( // coordinates: samplePedestrianWaypoints.first!, diff --git a/apple/Sources/FerrostarCore/NavigationState.swift b/apple/Sources/FerrostarCore/NavigationState.swift index c44f1d55..911af52b 100644 --- a/apple/Sources/FerrostarCore/NavigationState.swift +++ b/apple/Sources/FerrostarCore/NavigationState.swift @@ -7,35 +7,34 @@ import Foundation /// While the core generally does not include UI, this is purely at the model layer and should be implemented /// the same for all frontends. public struct NavigationState: Hashable { - public internal(set) var tripState: TripState public internal(set) var routeGeometry: [GeographicCoordinate] - + // TODO: This probably gets removed once we have an observer protocol - + /// Indicates when the core is calculating a new route due to the user being off route public internal(set) var isCalculatingNewRoute: Bool = false - + init(tripState: TripState, routeGeometry: [GeographicCoordinate], isCalculatingNewRoute: Bool = false) { self.tripState = tripState self.routeGeometry = routeGeometry self.isCalculatingNewRoute = isCalculatingNewRoute } - + // TODO: Delete once using tripState // public internal(set) var snappedLocation: UserLocation // public internal(set) var routeDeviation: RouteDeviation? // public internal(set) var spokenInstruction: SpokenInstruction? // public internal(set) var visualInstruction: VisualInstruction? // public internal(set) var progress: TripProgress? - + // TODO: This may be part of TripState? // public internal(set) var heading: Heading? // public internal(set) var currentStep: RouteStep? - + // TODO: Delete once using routeGeometry // public internal(set) var fullRouteShape: [GeographicCoordinate] - + // init( // snappedLocation: UserLocation, // heading: Heading? = nil, diff --git a/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift index 33e137f3..0b892eb2 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift @@ -137,7 +137,7 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { return EmptyView() } - + return DynamicallyOrientingNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), @@ -156,7 +156,7 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { return EmptyView() } - + return DynamicallyOrientingNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), diff --git a/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift index 295ab2f0..514085f4 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift @@ -106,7 +106,7 @@ public struct LandscapeNavigationView: View { guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { return EmptyView() } - + return LandscapeNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), @@ -127,7 +127,7 @@ public struct LandscapeNavigationView: View { guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { return EmptyView() } - + return LandscapeNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), diff --git a/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift b/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift index 2bd4753a..16f7f8ca 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift @@ -101,7 +101,7 @@ public struct NavigationMapView: View { guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { return EmptyView() } - + return NavigationMapView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), diff --git a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift index a34318c3..7fe65204 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift @@ -47,8 +47,9 @@ struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView var body: some View { HStack { VStack { - if case let .navigating(_, _, _, progress: progress, _, visualInstruction: visualInstruction, _) = navigationState?.tripState, - let visualInstruction + if case let .navigating(_, _, _, progress: progress, _, visualInstruction: visualInstruction, + _) = navigationState?.tripState, + let visualInstruction { InstructionsView( visualInstruction: visualInstruction, diff --git a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift index 53abafe7..8814af19 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift @@ -46,8 +46,9 @@ struct PortraitNavigationOverlayView: View, CustomizableNavigatingInnerGridView var body: some View { VStack { - if case let .navigating(_, _, _, progress: progress, _, visualInstruction: visualInstruction, _) = navigationState?.tripState, - let visualInstruction + if case let .navigating(_, _, _, progress: progress, _, visualInstruction: visualInstruction, + _) = navigationState?.tripState, + let visualInstruction { InstructionsView( visualInstruction: visualInstruction, diff --git a/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift index 19f42636..a256f076 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift @@ -107,7 +107,7 @@ public struct PortraitNavigationView: View, CustomizableNavigatingInnerGridView guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { return EmptyView() } - + return PortraitNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), @@ -127,7 +127,7 @@ public struct PortraitNavigationView: View, CustomizableNavigatingInnerGridView guard case let .navigating(snappedUserLocation: userLocation, _, _, _, _, _, _) = state.tripState else { return EmptyView() } - + return PortraitNavigationView( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!, camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)), diff --git a/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift b/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift index 353f7183..6fd92008 100644 --- a/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift +++ b/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift @@ -99,7 +99,7 @@ final class FerrostarCoreTests: XCTestCase { let core = FerrostarCore( routeAdapter: routeAdapter, - locationProvider: SimulatedLocationProvider(), + locationProvider: SimulatedLocationProvider(), navigationControllerConfig: .init(stepAdvance: .manual, routeDeviationTracking: .none), networkSession: mockSession ) @@ -194,7 +194,7 @@ final class FerrostarCoreTests: XCTestCase { let core = FerrostarCore( customRouteProvider: mockCustomRouteProvider, - locationProvider: SimulatedLocationProvider(), + locationProvider: SimulatedLocationProvider(), navigationControllerConfig: .init(stepAdvance: .manual, routeDeviationTracking: .none), networkSession: mockSession ) @@ -231,7 +231,7 @@ final class FerrostarCoreTests: XCTestCase { let core = FerrostarCore( routeAdapter: mockRouteAdapter, - locationProvider: locationProvider, + locationProvider: locationProvider, navigationControllerConfig: .init(stepAdvance: .manual, routeDeviationTracking: .none), networkSession: mockSession ) diff --git a/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift b/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift index 7611e104..52dc8f6c 100644 --- a/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift +++ b/apple/Tests/FerrostarCoreTests/ValhallaCoreTests.swift @@ -17,7 +17,7 @@ final class ValhallaCoreTests: XCTestCase { let core = try FerrostarCore( valhallaEndpointUrl: valhallaEndpointUrl, profile: "auto", - locationProvider: SimulatedLocationProvider(), + locationProvider: SimulatedLocationProvider(), navigationControllerConfig: .init(stepAdvance: .manual, routeDeviationTracking: .none), networkSession: mockSession ) From eb8559e1fecbbbf57448bee20a9253c281f342ca Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 21:26:28 -0700 Subject: [PATCH 05/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- android/composeui/build.gradle | 2 +- android/core/build.gradle | 2 +- android/maplibreui/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/composeui/build.gradle b/android/composeui/build.gradle index 6cd63ef0..b24da458 100644 --- a/android/composeui/build.gradle +++ b/android/composeui/build.gradle @@ -11,7 +11,7 @@ android { compileSdk 34 defaultConfig { - minSdk 26 + minSdk 25 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/android/core/build.gradle b/android/core/build.gradle index 72f67265..fa5b5695 100644 --- a/android/core/build.gradle +++ b/android/core/build.gradle @@ -11,7 +11,7 @@ android { ndkVersion "26.2.11394342" defaultConfig { - minSdk 26 + minSdk 25 targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/maplibreui/build.gradle b/android/maplibreui/build.gradle index bae70409..7707412c 100644 --- a/android/maplibreui/build.gradle +++ b/android/maplibreui/build.gradle @@ -10,7 +10,7 @@ android { compileSdk 34 defaultConfig { - minSdk 26 + minSdk 25 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From d7298f4cfc1c216ad893f9d7ee96395e467cbe13 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 21:28:16 -0700 Subject: [PATCH 06/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- .../FerrostarCore/NavigationState.swift | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/apple/Sources/FerrostarCore/NavigationState.swift b/apple/Sources/FerrostarCore/NavigationState.swift index 911af52b..987695f5 100644 --- a/apple/Sources/FerrostarCore/NavigationState.swift +++ b/apple/Sources/FerrostarCore/NavigationState.swift @@ -20,34 +20,4 @@ public struct NavigationState: Hashable { self.routeGeometry = routeGeometry self.isCalculatingNewRoute = isCalculatingNewRoute } - - // TODO: Delete once using tripState -// public internal(set) var snappedLocation: UserLocation -// public internal(set) var routeDeviation: RouteDeviation? -// public internal(set) var spokenInstruction: SpokenInstruction? -// public internal(set) var visualInstruction: VisualInstruction? -// public internal(set) var progress: TripProgress? - - // TODO: This may be part of TripState? -// public internal(set) var heading: Heading? -// public internal(set) var currentStep: RouteStep? - - // TODO: Delete once using routeGeometry -// public internal(set) var fullRouteShape: [GeographicCoordinate] - -// init( -// snappedLocation: UserLocation, -// heading: Heading? = nil, -// fullRouteShape: [GeographicCoordinate], -// steps: [RouteStep], -// progress: TripProgress? = nil -// ) { -// self.snappedLocation = snappedLocation -// self.heading = heading -// self.fullRouteShape = fullRouteShape -// self.progress = progress -// currentStep = steps.first -// visualInstruction = currentStep?.visualInstructions.first -// spokenInstruction = currentStep?.spokenInstructions.first -// } } From fa56ed4183913329a4cf70eb4defeae10418d01f Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 21:29:56 -0700 Subject: [PATCH 07/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- .../Mock/MockNavigationState.swift | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift b/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift index 2577f228..f46955f7 100644 --- a/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift +++ b/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift @@ -34,17 +34,14 @@ public extension NavigationState { let remainingLocations = Array(samplePedestrianWaypoints.dropFirst(n)) let lastUserLocation = remainingLocations.first! - var result = NavigationState( + return NavigationState( tripState: .navigating( snappedUserLocation: UserLocation( - latitude: samplePedestrianWaypoints.first!.lat, - longitude: samplePedestrianWaypoints.first!.lng, - horizontalAccuracy: 0, - course: 0, - courseAccuracy: 0, + coordinates: samplePedestrianWaypoints.first!, + horizontalAccuracy: 10, + courseOverGround: CourseOverGround(degrees: 0, accuracy: 10), timestamp: Date(), - speed: 0, - speedAccuracy: 0 + speed: Speed(value: 0, accuracy: 2) ), remainingSteps: [ RouteStep( @@ -80,17 +77,6 @@ public extension NavigationState { routeGeometry: samplePedestrianWaypoints, isCalculatingNewRoute: false ) - - // TODO: Move this to the NavigationState? -// result.snappedLocation = UserLocation( -// coordinates: samplePedestrianWaypoints.first!, -// horizontalAccuracy: 10, -// courseOverGround: CourseOverGround(degrees: 0, accuracy: 10), -// timestamp: Date(), -// speed: Speed(value: 0, accuracy: 2) -// ) - - return result } } From cea3090f789da9d895f1e3442f531916dd210fa7 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 21:35:14 -0700 Subject: [PATCH 08/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- .../java/com/stadiamaps/ferrostar/core/FerrostarCore.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt index 7b8eade7..e426769e 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt @@ -124,8 +124,8 @@ class FerrostarCore( /** * The current state of the navigation session. This can be used in a custom ViewModel or - * elsewhere. If using the default behavior, use the NavigationViewModel provided by - * startNavigation(). + * elsewhere. If using the default behavior, use the DefaultNavigationViewModel by injection or + * as provided by startNavigation(). */ var state: StateFlow = _state.asStateFlow() @@ -222,7 +222,7 @@ class FerrostarCore( fun startNavigation( route: Route, config: NavigationControllerConfig? = null - ): NavigationViewModel { + ): DefaultNavigationViewModel { stopNavigation() // Apply the new config if provided, otherwise use the original. From d5fccdffcedd31ba6f8671d4043a20dec86d1d7d Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 21:39:02 -0700 Subject: [PATCH 09/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- .../main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt index e426769e..e25d8585 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt @@ -157,7 +157,7 @@ class FerrostarCore( constructor( customRouteProvider: CustomRouteProvider, - httpClient: OkHttpClient = OkHttpClient(), + httpClient: OkHttpClient, locationProvider: LocationProvider, navigationControllerConfig: NavigationControllerConfig, ) : this( From 2177d04830f88edd56e3c59fd758220849ae913f Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 21:45:10 -0700 Subject: [PATCH 10/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- .../java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index b243fe86..e559d9eb 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -44,6 +44,7 @@ data class NavigationUiState( interface NavigationViewModel { val uiState: StateFlow + fun stopNavigation() } class DefaultNavigationViewModel( @@ -80,7 +81,7 @@ class DefaultNavigationViewModel( started = SharingStarted.WhileSubscribed(), initialValue = uiState(ferrostarCore.state.value, lastLocation)) - fun stopNavigation() { + override fun stopNavigation() { ferrostarCore.stopNavigation() } From 24bd33a900fbb78c0e19588eb1916592752993e6 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 29 Jul 2024 22:05:33 -0700 Subject: [PATCH 11/15] Synced android and ios navigation state, refactored NavigationViewModel for better injection --- .../main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt | 4 ++-- .../java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt | 1 + .../com/stadiamaps/ferrostar/core/mock/MockNavigationState.kt | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt index e25d8585..95e0e829 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt @@ -124,8 +124,8 @@ class FerrostarCore( /** * The current state of the navigation session. This can be used in a custom ViewModel or - * elsewhere. If using the default behavior, use the DefaultNavigationViewModel by injection or - * as provided by startNavigation(). + * elsewhere. If using the default behavior, use the DefaultNavigationViewModel by injection or as + * provided by startNavigation(). */ var state: StateFlow = _state.asStateFlow() diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index e559d9eb..1a92be0d 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -44,6 +44,7 @@ data class NavigationUiState( interface NavigationViewModel { val uiState: StateFlow + fun stopNavigation() } diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/mock/MockNavigationState.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/mock/MockNavigationState.kt index 62b9bda6..3838b61f 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/mock/MockNavigationState.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/mock/MockNavigationState.kt @@ -62,4 +62,6 @@ fun NavigationUiState.Companion.pedestrianExample(): NavigationUiState = fromFerrostar(NavigationState.pedestrianExample(), UserLocation.pedestrianExample()) class MockNavigationViewModel(override val uiState: StateFlow) : - ViewModel(), NavigationViewModel + ViewModel(), NavigationViewModel { + override fun stopNavigation() {} +} From 02425ce79618f71181e69e4cd32c79ebaa21c2a5 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Tue, 30 Jul 2024 07:17:02 -0700 Subject: [PATCH 12/15] Correct CI failures, updated rust semver to 0.6.0 --- CONTRIBUTING.md | 2 ++ Package.swift | 2 +- .../ferrostar/core/FerrostarCoreTest.kt | 16 ++++++++++---- .../ferrostar/core/ValhallaCoreTest.kt | 9 +++++++- apple/DemoApp/Demo/DemoNavigationView.swift | 21 +++++++++---------- common/Cargo.lock | 2 +- common/ferrostar/Cargo.toml | 2 +- 7 files changed, 35 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9459cc1c..9ef1ef13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,6 +79,8 @@ Before pushing, run the following in the `common` folder: 1. Run `cargo fmt` to automatically format any new rust code. 2. Run `cargo insta review` to update snapshot testing changes. 3. Run `cargo test` to validate testing and ensure snapshot changes were correctly applied by step 2. +4. Manually bump the version on the ferrostar Cargo.toml at `common/ferrostar/Cargo.toml`. This will fail the rust semver check +if you don't. ### Web diff --git a/Package.swift b/Package.swift index 06e2af6c..2570780e 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let binaryTarget: Target let maplibreSwiftUIDSLPackage: Package.Dependency -let useLocalFramework = false +let useLocalFramework = true let useLocalMapLibreSwiftUIDSL = false if useLocalFramework { diff --git a/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/FerrostarCoreTest.kt b/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/FerrostarCoreTest.kt index 82596214..a5968c72 100644 --- a/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/FerrostarCoreTest.kt +++ b/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/FerrostarCoreTest.kt @@ -108,7 +108,9 @@ class FerrostarCoreTest { requestGenerator = MockRouteRequestGenerator(), responseParser = MockRouteResponseParser(routes = listOf())), httpClient = OkHttpClient.Builder().addInterceptor(interceptor).build(), - locationProvider = SimulatedLocationProvider()) + locationProvider = SimulatedLocationProvider(), + navigationControllerConfig = + NavigationControllerConfig(StepAdvanceMode.Manual, RouteDeviationTracking.None)) try { // Tests that the core generates a request and attempts to process it, but throws due to the @@ -152,7 +154,9 @@ class FerrostarCoreTest { requestGenerator = MockRouteRequestGenerator(), responseParser = MockRouteResponseParser(routes = listOf(mockRoute))), httpClient = OkHttpClient.Builder().addInterceptor(interceptor).build(), - locationProvider = SimulatedLocationProvider()) + locationProvider = SimulatedLocationProvider(), + navigationControllerConfig = + NavigationControllerConfig(StepAdvanceMode.Manual, RouteDeviationTracking.None)) val routes = core.getRoutes( initialLocation = @@ -199,7 +203,9 @@ class FerrostarCoreTest { FerrostarCore( customRouteProvider = routeProvider, httpClient = OkHttpClient.Builder().addInterceptor(interceptor).build(), - locationProvider = SimulatedLocationProvider()) + locationProvider = SimulatedLocationProvider(), + navigationControllerConfig = + NavigationControllerConfig(StepAdvanceMode.Manual, RouteDeviationTracking.None)) val routes = core.getRoutes( initialLocation = @@ -264,7 +270,9 @@ class FerrostarCoreTest { requestGenerator = MockRouteRequestGenerator(), responseParser = MockRouteResponseParser(routes = listOf(mockRoute))), httpClient = OkHttpClient.Builder().addInterceptor(interceptor).build(), - locationProvider = locationProvider) + locationProvider = locationProvider, + navigationControllerConfig = + NavigationControllerConfig(StepAdvanceMode.Manual, RouteDeviationTracking.None)) val deviationHandler = DeviationHandler() core.deviationHandler = deviationHandler diff --git a/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt b/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt index ff7f7b51..3f4ce614 100644 --- a/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt +++ b/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt @@ -24,6 +24,9 @@ import okhttp3.mock.url import org.junit.Assert.assertEquals import org.junit.Test import uniffi.ferrostar.GeographicCoordinate +import uniffi.ferrostar.NavigationControllerConfig +import uniffi.ferrostar.RouteDeviationTracking +import uniffi.ferrostar.StepAdvanceMode import uniffi.ferrostar.UserLocation import uniffi.ferrostar.Waypoint import uniffi.ferrostar.WaypointKind @@ -246,7 +249,9 @@ class ValhallaCoreTest { valhallaEndpointURL = URL(valhallaEndpointUrl), profile = "auto", httpClient = OkHttpClient.Builder().addInterceptor(interceptor).build(), - locationProvider = SimulatedLocationProvider()) + locationProvider = SimulatedLocationProvider(), + navigationControllerConfig = + NavigationControllerConfig(StepAdvanceMode.Manual, RouteDeviationTracking.None)) return runTest { val routes = @@ -291,6 +296,8 @@ class ValhallaCoreTest { profile = "auto", httpClient = OkHttpClient.Builder().addInterceptor(interceptor).build(), locationProvider = SimulatedLocationProvider(), + navigationControllerConfig = + NavigationControllerConfig(StepAdvanceMode.Manual, RouteDeviationTracking.None), costingOptions = mapOf("auto" to mapOf("useTolls" to 0))) return runTest { diff --git a/apple/DemoApp/Demo/DemoNavigationView.swift b/apple/DemoApp/Demo/DemoNavigationView.swift index f4b4fbc1..4a1aa03e 100644 --- a/apple/DemoApp/Demo/DemoNavigationView.swift +++ b/apple/DemoApp/Demo/DemoNavigationView.swift @@ -39,12 +39,21 @@ struct DemoNavigationView: View { let simulated = SimulatedLocationProvider(location: initialLocation) simulated.warpFactor = 2 locationProvider = simulated + + // Configure the navigation session. + // You have a lot of flexibility here based on your use case. + let config = SwiftNavigationControllerConfig( + stepAdvance: .relativeLineStringDistance(minimumHorizontalAccuracy: 32, automaticAdvanceDistance: 10), + routeDeviationTracking: .staticThreshold(minimumHorizontalAccuracy: 25, maxAcceptableDeviation: 20) + ) + ferrostarCore = try! FerrostarCore( valhallaEndpointUrl: URL( string: "https://api.stadiamaps.com/route/v1?api_key=\(APIKeys.shared.stadiaMapsAPIKey)" )!, profile: "bicycle", locationProvider: locationProvider, + navigationControllerConfig: config, costingOptions: ["bicycle": ["use_roads": 0.2]] ) // NOTE: Not all applications will need a delegate. Read the NavigationDelegate documentation for details. @@ -177,13 +186,6 @@ struct DemoNavigationView: View { return } - // Configure the navigation session. - // You have a lot of flexibility here based on your use case. - let config = SwiftNavigationControllerConfig( - stepAdvance: .relativeLineStringDistance(minimumHorizontalAccuracy: 32, automaticAdvanceDistance: 10), - routeDeviationTracking: .staticThreshold(minimumHorizontalAccuracy: 25, maxAcceptableDeviation: 20) - ) - if let simulated = locationProvider as? SimulatedLocationProvider { // This configures the simulator to the desired route. // The ferrostarCore.startNavigation will still start the location @@ -195,10 +197,7 @@ struct DemoNavigationView: View { // Starts the navigation state machine. // It's worth having a look through the parameters, // as most of the configuration happens here. - try ferrostarCore.startNavigation( - route: route, - config: config - ) + try ferrostarCore.startNavigation(route: route) preventAutoLock() } diff --git a/common/Cargo.lock b/common/Cargo.lock index de9fa973..d801e9c4 100644 --- a/common/Cargo.lock +++ b/common/Cargo.lock @@ -328,7 +328,7 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "ferrostar" -version = "0.5.0" +version = "0.6.0" dependencies = [ "assert-json-diff", "geo", diff --git a/common/ferrostar/Cargo.toml b/common/ferrostar/Cargo.toml index e4303de7..a8eb8db6 100644 --- a/common/ferrostar/Cargo.toml +++ b/common/ferrostar/Cargo.toml @@ -2,7 +2,7 @@ lints.workspace = true [package] name = "ferrostar" -version = "0.5.0" +version = "0.6.0" readme = "README.md" description = "The core of modern turn-by-turn navigation." keywords = ["navigation", "routing", "valhalla", "osrm"] From d9a15207c1865458413a9606fbd1067985eeaef3 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Tue, 30 Jul 2024 19:11:38 -0700 Subject: [PATCH 13/15] Revisions from discussion --- Package.swift | 2 +- .../stadiamaps/ferrostar/core/FerrostarCore.kt | 3 ++- .../ferrostar/core/NavigationViewModel.kt | 17 +++++------------ .../stadiamaps/ferrostar/DemoNavigationScene.kt | 17 ++++++++--------- .../ferrostar/maplibreui/NavigationMapView.kt | 2 +- .../src/navigation_controller/models.rs | 5 +++-- 6 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Package.swift b/Package.swift index 2570780e..06e2af6c 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let binaryTarget: Target let maplibreSwiftUIDSLPackage: Package.Dependency -let useLocalFramework = true +let useLocalFramework = false let useLocalMapLibreSwiftUIDSL = false if useLocalFramework { diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt index 95e0e829..a8d54106 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt @@ -213,7 +213,8 @@ class FerrostarCore( * model is "orphaned"! * * @param route the route to navigate. - * @param config Override the configuration for the navigation session. This was provided on init. + * @param config change the configuration in the core before staring navigation. This was + * originally provided on init, but you can set a new value for future sessions. * @return a view model tied to the navigation session. This can be ignored if you're injecting * the [NavigationViewModel]/[DefaultNavigationViewModel]. * @throws UserLocationUnknown if the location provider has no last known location. diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index 1a92be0d..17cb01e0 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -18,7 +18,7 @@ import uniffi.ferrostar.UserLocation import uniffi.ferrostar.VisualInstruction data class NavigationUiState( - val snappedLocation: UserLocation, + val snappedLocation: UserLocation?, val heading: Float?, val routeGeometry: List, val visualInstruction: VisualInstruction?, @@ -28,7 +28,7 @@ data class NavigationUiState( val routeDeviation: RouteDeviation? ) { companion object { - fun fromFerrostar(coreState: NavigationState, userLocation: UserLocation): NavigationUiState = + fun fromFerrostar(coreState: NavigationState, userLocation: UserLocation?): NavigationUiState = NavigationUiState( snappedLocation = userLocation, // TODO: Heading/course over ground @@ -53,14 +53,7 @@ class DefaultNavigationViewModel( locationProvider: LocationProvider ) : ViewModel(), NavigationViewModel { - private var lastLocation: UserLocation - - init { - lastLocation = - requireNotNull(locationProvider.lastLocation) { - "LocationProvider must have a last location." - } - } + private var lastLocation: UserLocation? = locationProvider.lastLocation override val uiState = ferrostarCore.state @@ -69,7 +62,7 @@ class DefaultNavigationViewModel( when (coreState.tripState) { is TripState.Navigating -> coreState.tripState.snappedUserLocation is TripState.Complete, - TripState.Idle -> lastLocation + TripState.Idle -> locationProvider.lastLocation } uiState(coreState, lastLocation) @@ -86,6 +79,6 @@ class DefaultNavigationViewModel( ferrostarCore.stopNavigation() } - private fun uiState(coreState: NavigationState, location: UserLocation) = + private fun uiState(coreState: NavigationState, location: UserLocation?) = NavigationUiState.fromFerrostar(coreState, location) } diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt index 8dd58e81..7dfe4e9b 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt @@ -100,15 +100,14 @@ fun DemoNavigationScene( // Trivial, if silly example of how to add your own overlay layers. // (Also incidentally highlights the lag inherent in MapLibre location tracking // as-is.) - Circle( - center = - LatLng( - uiState.value.snappedLocation.coordinates.lat, - uiState.value.snappedLocation.coordinates.lng), - radius = 10f, - color = "Blue", - zIndex = 2, - ) + uiState.value.snappedLocation?.let { + Circle( + center = LatLng(it.coordinates.lat, it.coordinates.lng), + radius = 10f, + color = "Blue", + zIndex = 2, + ) + } } } else { // Loading indicator diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt index 683d54b1..79b2bae9 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt @@ -48,7 +48,7 @@ fun NavigationMapView( val uiState = viewModel.uiState.collectAsState() val locationEngine = remember { StaticLocationEngine() } - locationEngine.lastLocation = uiState.value.snappedLocation.toAndroidLocation() + locationEngine.lastLocation = uiState.value.snappedLocation?.toAndroidLocation() MapView( modifier = Modifier.fillMaxSize(), diff --git a/common/ferrostar/src/navigation_controller/models.rs b/common/ferrostar/src/navigation_controller/models.rs index 9588abab..ca427293 100644 --- a/common/ferrostar/src/navigation_controller/models.rs +++ b/common/ferrostar/src/navigation_controller/models.rs @@ -29,9 +29,10 @@ pub struct TripProgress { #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[cfg_attr(feature = "wasm-bindgen", derive(Serialize, Deserialize))] pub enum TripState { - /// The navigation controller is idle + /// The navigation controller is idle and there is no active trip. Idle, #[cfg_attr(feature = "wasm-bindgen", serde(rename_all = "camelCase"))] + /// The navigation controller is actively navigating a trip. Navigating { snapped_user_location: UserLocation, /// The ordered list of steps that remain in the trip. @@ -60,7 +61,7 @@ pub enum TripState { /// Note it is the responsibility of the platform layer to ensure that utterances are not synthesized multiple times. This property simply reports the current spoken instruction. spoken_instruction: Option, }, - /// The navigation controller has reached the end of the route. + /// The navigation controller has reached the end of the trip. Complete, } From bf481d5ea57a14635af74d5506a9be5c79ca0025 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Tue, 30 Jul 2024 19:21:19 -0700 Subject: [PATCH 14/15] Fixed android version --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 73ebd272..ff7919a3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,5 +25,5 @@ publishing { allprojects { group = "com.stadiamaps.ferrostar" - version = "0.5.0" + version = "0.6.0" } From 72a70b2e8b4f34d669305e74a42fab0431a47fb8 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 31 Jul 2024 11:22:27 +0900 Subject: [PATCH 15/15] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ef1ef13..c84da7f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,7 +79,7 @@ Before pushing, run the following in the `common` folder: 1. Run `cargo fmt` to automatically format any new rust code. 2. Run `cargo insta review` to update snapshot testing changes. 3. Run `cargo test` to validate testing and ensure snapshot changes were correctly applied by step 2. -4. Manually bump the version on the ferrostar Cargo.toml at `common/ferrostar/Cargo.toml`. This will fail the rust semver check +4. Manually bump the version on the ferrostar Cargo.toml at `common/ferrostar/Cargo.toml`. If you forget to do this and make breaking changes, CI will fail. if you don't. ### Web