Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Android Support for Make display of snapped location configurab… #250

Merged
merged 3 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,45 @@ import uniffi.ferrostar.UserLocation
import uniffi.ferrostar.VisualInstruction

data class NavigationUiState(
/** The user's location as reported by the location provider. */
val location: UserLocation?,
/** The user's location snapped to the route shape. */
val snappedLocation: UserLocation?,
/**
* The last known heading of the user.
*
* NOTE: This is distinct from the course over ground (direction of travel), which is included
* in the `location` and `snappedLocation` properties.
*/
val heading: Float?,
/** The geometry of the full route. */
val routeGeometry: List<GeographicCoordinate>,
/** Visual instructions which should be displayed based on the user's current progress. */
val visualInstruction: VisualInstruction?,
/**
* Instructions which should be spoken via speech synthesis based on the user's current
* progress.
*/
val spokenInstruction: SpokenInstruction?,
/** The user's progress through the current trip. */
val progress: TripProgress?,
/** If true, the core is currently calculating a new route. */
val isCalculatingNewRoute: Boolean?,
/** Describes whether the user is believed to be off the correct route. */
val routeDeviation: RouteDeviation?,
/** If true, spoken instructions will not be synthesized. */
val isMuted: Boolean?
) {
companion object {
fun fromFerrostar(
coreState: NavigationState,
isMuted: Boolean?,
userLocation: UserLocation?
location: UserLocation?,
snappedLocation: UserLocation?
): NavigationUiState =
NavigationUiState(
snappedLocation = userLocation,
snappedLocation = snappedLocation,
location = location,
// TODO: Heading/course over ground
heading = null,
routeGeometry = coreState.routeGeometry,
Expand All @@ -60,22 +81,22 @@ interface NavigationViewModel {
class DefaultNavigationViewModel(
private val ferrostarCore: FerrostarCore,
private val spokenInstructionObserver: SpokenInstructionObserver? = null,
locationProvider: LocationProvider
private val locationProvider: LocationProvider
) : ViewModel(), NavigationViewModel {

private var lastLocation: UserLocation? = locationProvider.lastLocation
private var userLocation: UserLocation? = locationProvider.lastLocation

override val uiState =
ferrostarCore.state
.map { coreState ->
lastLocation =
val location = locationProvider.lastLocation
userLocation =
when (coreState.tripState) {
ianthetechie marked this conversation as resolved.
Show resolved Hide resolved
is TripState.Navigating -> coreState.tripState.snappedUserLocation
is TripState.Complete,
TripState.Idle -> locationProvider.lastLocation
}

uiState(coreState, spokenInstructionObserver?.isMuted, lastLocation)
uiState(coreState, spokenInstructionObserver?.isMuted, location, userLocation)
// This awkward dance is required because Kotlin doesn't have a way to map over
// StateFlows
// without converting to a generic Flow in the process.
Expand All @@ -85,7 +106,10 @@ class DefaultNavigationViewModel(
started = SharingStarted.WhileSubscribed(),
initialValue =
uiState(
ferrostarCore.state.value, spokenInstructionObserver?.isMuted, lastLocation))
ferrostarCore.state.value,
spokenInstructionObserver?.isMuted,
locationProvider.lastLocation,
userLocation))

override fun stopNavigation() {
ferrostarCore.stopNavigation()
Expand All @@ -99,6 +123,10 @@ class DefaultNavigationViewModel(
spokenInstructionObserver.isMuted = !spokenInstructionObserver.isMuted
}

private fun uiState(coreState: NavigationState, isMuted: Boolean?, location: UserLocation?) =
NavigationUiState.fromFerrostar(coreState, isMuted, location)
private fun uiState(
coreState: NavigationState,
isMuted: Boolean?,
location: UserLocation?,
snappedLocation: UserLocation?
) = NavigationUiState.fromFerrostar(coreState, isMuted, location, snappedLocation)
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ fun NavigationState.Companion.pedestrianExample(): NavigationState {
}

fun NavigationUiState.Companion.pedestrianExample(): NavigationUiState =
fromFerrostar(NavigationState.pedestrianExample(), false, UserLocation.pedestrianExample())
fromFerrostar(
NavigationState.pedestrianExample(),
false,
UserLocation.pedestrianExample(),
UserLocation.pedestrianExample())

class MockNavigationViewModel(override val uiState: StateFlow<NavigationUiState>) :
ViewModel(), NavigationViewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ fun DemoNavigationScene(
// Most vendors offer free API keys for development use.
styleUrl = "https://demotiles.maplibre.org/style.json",
viewModel = viewModel!!,
// This is the default value, which works well for motor vehicle navigation.
// Other travel modes though, such as walking, may not want snapping.
snapUserLocationToRoute = true,
onTapExit = { viewModel!!.stopNavigation() }) { uiState ->
// Trivial, if silly example of how to add your own overlay layers.
// (Also incidentally highlights the lag inherent in MapLibre location tracking
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.mapbox.mapboxsdk.geometry.LatLng
Expand All @@ -30,6 +29,8 @@ import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera
* @param viewModel The navigation view model provided by Ferrostar Core.
* @param locationRequestProperties The location request properties to use for the map's location
* engine.
* @param snapUserLocationToRoute If true, the user's displayed location will be snapped to the
* route line.
* @param onMapReadyCallback A callback that is invoked when the map is ready to be interacted with.
* You must set your desired MapViewCamera tracking mode here!
* @param content Any additional composable map symbol content to render.
Expand All @@ -43,13 +44,19 @@ fun NavigationMapView(
viewModel: NavigationViewModel,
locationRequestProperties: LocationRequestProperties =
LocationRequestProperties.NavigationDefault(),
snapUserLocationToRoute: Boolean = true,
onMapReadyCallback: (Style) -> Unit = { camera.value = navigationCamera },
content: @Composable @MapLibreComposable() ((State<NavigationUiState>) -> Unit)? = null
) {
val uiState = viewModel.uiState.collectAsState()

val locationEngine = remember { StaticLocationEngine() }
locationEngine.lastLocation = uiState.value.snappedLocation?.toAndroidLocation()
locationEngine.lastLocation =
if (snapUserLocationToRoute) {
uiState.value.snappedLocation?.toAndroidLocation()
} else {
uiState.value.location?.toAndroidLocation()
}

MapView(
modifier = Modifier.fillMaxSize(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import com.stadiamaps.ferrostar.maplibreui.views.overlays.PortraitNavigationOver
* @param viewModel The navigation view model provided by Ferrostar Core.
* @param locationRequestProperties The location request properties to use for the map's location
* engine.
* @param snapUserLocationToRoute If true, the user's displayed location will be snapped to the
* route line.
* @param config The configuration for the navigation view.
* @param landscapeOverlayModifier The modifier to apply to the overlay view.
* @param portraitOverlayModifier The modifier to apply to the overlay view.
Expand All @@ -51,6 +53,7 @@ fun DynamicallyOrientingNavigationView(
viewModel: NavigationViewModel,
locationRequestProperties: LocationRequestProperties =
LocationRequestProperties.NavigationDefault(),
snapUserLocationToRoute: Boolean = true,
config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(),
landscapeOverlayModifier: Modifier = Modifier.fillMaxSize().padding(16.dp),
portraitOverlayModifier: Modifier = Modifier.fillMaxSize().padding(16.dp),
Expand All @@ -68,6 +71,7 @@ fun DynamicallyOrientingNavigationView(
navigationCamera,
viewModel,
locationRequestProperties,
snapUserLocationToRoute,
onMapReadyCallback = { camera.value = navigationCamera },
content)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import kotlinx.coroutines.flow.asStateFlow
* @param viewModel The navigation view model provided by Ferrostar Core.
* @param locationRequestProperties The location request properties to use for the map's location
* engine.
* @param snapUserLocationToRoute If true, the user's displayed location will be snapped to the
* route line.
* @param config The configuration for the navigation view.
* @param overlayModifier The modifier to apply to the overlay view.
* @param onTapExit The callback to invoke when the exit button is tapped.
Expand All @@ -52,6 +54,7 @@ fun LandscapeNavigationView(
viewModel: NavigationViewModel,
locationRequestProperties: LocationRequestProperties =
LocationRequestProperties.NavigationDefault(),
snapUserLocationToRoute: Boolean = true,
config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(),
overlayModifier: Modifier = Modifier.fillMaxSize().padding(16.dp),
onTapExit: (() -> Unit)? = null,
Expand All @@ -65,6 +68,7 @@ fun LandscapeNavigationView(
navigationCamera,
viewModel,
locationRequestProperties,
snapUserLocationToRoute,
onMapReadyCallback = { camera.value = navigationCamera },
content)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import kotlinx.coroutines.flow.asStateFlow
* @param viewModel The navigation view model provided by Ferrostar Core.
* @param locationRequestProperties The location request properties to use for the map's location
* engine.
* @param snapUserLocationToRoute If true, the user's displayed location will be snapped to the
* route line.
* @param config The configuration for the navigation view.
* @param overlayModifier The modifier to apply to the overlay view.
* @param onTapExit The callback to invoke when the exit button is tapped.
Expand All @@ -53,6 +55,7 @@ fun PortraitNavigationView(
viewModel: NavigationViewModel,
locationRequestProperties: LocationRequestProperties =
LocationRequestProperties.NavigationDefault(),
snapUserLocationToRoute: Boolean = true,
config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(),
overlayModifier: Modifier = Modifier.fillMaxSize().padding(16.dp),
onTapExit: (() -> Unit)? = null,
Expand All @@ -66,6 +69,7 @@ fun PortraitNavigationView(
navigationCamera,
viewModel,
locationRequestProperties,
snapUserLocationToRoute,
onMapReadyCallback = { camera.value = navigationCamera },
content)

Expand Down
Loading