diff --git a/.github/workflows/cargo-publish.yml b/.github/workflows/cargo-publish.yml index d8235875..27a53d68 100644 --- a/.github/workflows/cargo-publish.yml +++ b/.github/workflows/cargo-publish.yml @@ -9,7 +9,7 @@ env: jobs: publish_crate: - runs-on: macos-14 + runs-on: macos-15 steps: - uses: actions/checkout@v4 - name: Cargo Publish diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index 0f6f2bd8..1d81e053 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -9,7 +9,7 @@ on: jobs: build: - runs-on: macos-13 + runs-on: ubuntu-latest permissions: contents: read packages: read diff --git a/.github/workflows/ios-release.yml b/.github/workflows/ios-release.yml index cc440909..7de934d3 100644 --- a/.github/workflows/ios-release.yml +++ b/.github/workflows/ios-release.yml @@ -6,7 +6,7 @@ on: jobs: ios-release: - runs-on: macos-14 + runs-on: macos-15 permissions: contents: write diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 3b5228e5..81cc7e19 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -8,7 +8,7 @@ on: jobs: format-lint: - runs-on: macos-14 + runs-on: macos-15 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-ios-swiftformat cancel-in-progress: true @@ -24,7 +24,7 @@ jobs: run: swiftformat . --lint build-ferrostar: - runs-on: macos-14 + runs-on: macos-15 permissions: contents: write # To auto-commit Package.swift and binding changes @@ -68,7 +68,7 @@ jobs: retention-days: 5 build-demo: - runs-on: macos-14 + runs-on: macos-15 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-ios-build-demo cancel-in-progress: true @@ -119,7 +119,7 @@ jobs: retention-days: 5 test: - runs-on: macos-14 + runs-on: macos-15 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-ios-test cancel-in-progress: true @@ -131,7 +131,7 @@ jobs: ] destination: [ # TODO: Add more destinations - 'platform=iOS Simulator,name=iPhone 15,OS=17.2' + 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.1' ] steps: diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 2de73fad..3f4b450b 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -10,7 +10,7 @@ env: jobs: build: - runs-on: macos-13 + runs-on: macos-15 steps: - name: Checkout code diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b17d6f60..daca7a0f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -12,7 +12,7 @@ env: jobs: msrv: - runs-on: macos-14 + runs-on: macos-15 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-rust-msrv cancel-in-progress: true @@ -27,7 +27,7 @@ jobs: working-directory: common semver-checks: - runs-on: macos-14 + runs-on: macos-15 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-rust-semver cancel-in-progress: true @@ -41,7 +41,7 @@ jobs: feature-group: default-features rustfmt: - runs-on: macos-14 + runs-on: macos-15 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-rust-rustfmt cancel-in-progress: true @@ -53,7 +53,7 @@ jobs: working-directory: common build: - runs-on: macos-14 + runs-on: macos-15 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-rust-build cancel-in-progress: true diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index a299c1dd..3f9f93ec 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -6,7 +6,7 @@ on: jobs: typos: - runs-on: macos-14 + runs-on: macos-15 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-ios-swiftformat cancel-in-progress: true diff --git a/.github/workflows/wasm-js.yml b/.github/workflows/wasm-js.yml index fc4837b6..26dd8708 100644 --- a/.github/workflows/wasm-js.yml +++ b/.github/workflows/wasm-js.yml @@ -12,7 +12,7 @@ env: jobs: build: - runs-on: macos-14 + runs-on: macos-15 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-wasm-build cancel-in-progress: true diff --git a/Package.swift b/Package.swift index 2928f9fd..f8b5c7d5 100644 --- a/Package.swift +++ b/Package.swift @@ -16,8 +16,8 @@ if useLocalFramework { path: "./common/target/ios/libferrostar-rs.xcframework" ) } else { - let releaseTag = "0.20.1" - let releaseChecksum = "a5d8ecc5b4d4b77e2e9fd237b3c6ffd62772606a996a0e34f0fc7ecadc19fb9c" + let releaseTag = "0.21.0" + let releaseChecksum = "1086da6011e474aaaae97555e33204afa6626df2f3c9042b2c1a91949d400066" binaryTarget = .binaryTarget( name: "FerrostarCoreRS", url: diff --git a/android/build.gradle b/android/build.gradle index 6b82729b..d6532152 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -17,5 +17,5 @@ ext { allprojects { group = "com.stadiamaps.ferrostar" - version = "0.20.1" + version = "0.21.0" } diff --git a/android/composeui/build.gradle b/android/composeui/build.gradle index 8e971cf7..5aafd9e9 100644 --- a/android/composeui/build.gradle +++ b/android/composeui/build.gradle @@ -12,7 +12,7 @@ plugins { android { namespace 'com.stadiamaps.ferrostar.composeui' - compileSdk 34 + compileSdk 35 defaultConfig { minSdk 25 diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/VisualNavigationViewConfig.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/VisualNavigationViewConfig.kt new file mode 100644 index 00000000..0ca3fb93 --- /dev/null +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/VisualNavigationViewConfig.kt @@ -0,0 +1,40 @@ +package com.stadiamaps.ferrostar.composeui.config + +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +sealed class CameraControlState { + data object Hidden : CameraControlState() + + data class ShowRecenter(val updateCamera: () -> Unit) : CameraControlState() + + data class ShowRouteOverview(val updateCamera: () -> Unit) : CameraControlState() +} + +data class VisualNavigationViewConfig( + var showMute: Boolean = false, + var showZoom: Boolean = false, + var buttonSize: DpSize = DpSize(56.dp, 56.dp) +) { + companion object { + fun Default() = VisualNavigationViewConfig(showMute = true, showZoom = true) + } +} + +/** Enables the mute button in the navigation view. */ +fun VisualNavigationViewConfig.useMuteButton(): VisualNavigationViewConfig { + showMute = true + return this +} + +/** Enables the zoom button in the navigation view. */ +fun VisualNavigationViewConfig.useZoomButton(): VisualNavigationViewConfig { + showZoom = true + return this +} + +/** Changes the size of navigation buttons. */ +fun VisualNavigationViewConfig.buttonSize(size: DpSize): VisualNavigationViewConfig { + buttonSize = size + return this +} diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/InstructionsView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/InstructionsView.kt index f57841d2..292574eb 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/InstructionsView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/InstructionsView.kt @@ -56,6 +56,7 @@ import uniffi.ferrostar.VisualInstructionContent fun InstructionsView( instructions: VisualInstruction, distanceToNextManeuver: Double?, + modifier: Modifier = Modifier, distanceFormatter: DistanceFormatter = LocalizedDistanceFormatter(), theme: InstructionRowTheme = DefaultInstructionRowTheme, remainingSteps: List? = null, @@ -69,7 +70,8 @@ fun InstructionsView( Box( modifier = - Modifier.fillMaxWidth() + modifier + .fillMaxWidth() .heightIn(max = screenHeight) .animateContentSize(animationSpec = spring(stiffness = Spring.StiffnessHigh)) .background(theme.backgroundColor, RoundedCornerShape(10.dp)) diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIButton.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIButton.kt index 50746f20..fcf8efa3 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIButton.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIButton.kt @@ -2,9 +2,8 @@ package com.stadiamaps.ferrostar.composeui.views.controls import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close @@ -18,6 +17,7 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import com.stadiamaps.ferrostar.composeui.R @@ -32,13 +32,14 @@ import com.stadiamaps.ferrostar.composeui.R @Composable fun NavigationUIButton( onClick: () -> Unit, + buttonSize: DpSize, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), content: @Composable () -> Unit ) { FloatingActionButton( onClick, - modifier = Modifier.width(56.dp).height(56.dp).shadow(6.dp, shape = CircleShape), + modifier = Modifier.size(buttonSize).shadow(6.dp, shape = CircleShape), shape = CircleShape, containerColor, contentColor) { @@ -50,7 +51,7 @@ fun NavigationUIButton( @Composable fun NavigationUIButtonPreview() { Box(Modifier.background(Color.LightGray).padding(16.dp)) { - NavigationUIButton({}) { + NavigationUIButton({}, DpSize(56.dp, 56.dp)) { Icon(Icons.Filled.Close, contentDescription = stringResource(id = R.string.end_navigation)) } } diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIZoomButton.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIZoomButton.kt index 37440102..cd3ee0a3 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIZoomButton.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIZoomButton.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -22,11 +23,13 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import com.stadiamaps.ferrostar.composeui.R @Composable fun NavigationUIZoomButton( + buttonSize: DpSize, onClickZoomIn: () -> Unit, onClickZoomOut: () -> Unit, containerColor: Color = FloatingActionButtonDefaults.containerColor, @@ -38,7 +41,7 @@ fun NavigationUIZoomButton( Column(modifier = Modifier.shadow(6.dp, shape = RoundedCornerShape(50))) { FloatingActionButton( onClick = onClickZoomIn, - modifier = Modifier.height(56.dp).width(56.dp), + modifier = Modifier.size(buttonSize), shape = RoundedCornerShape(topStartPercent = 50, topEndPercent = 50), containerColor = containerColor, contentColor = contentColor, @@ -48,13 +51,13 @@ fun NavigationUIZoomButton( contentDescription = stringResource(id = R.string.zoom_in)) } - Box(modifier = Modifier.height(1.dp).width(56.dp)) { + Box(modifier = Modifier.height(1.dp).width(buttonSize.width)) { HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant) } FloatingActionButton( onClick = onClickZoomOut, - modifier = Modifier.height(56.dp).width(56.dp), + modifier = Modifier.size(buttonSize), shape = RoundedCornerShape(bottomStartPercent = 50, bottomEndPercent = 50), containerColor = containerColor, contentColor = contentColor, @@ -69,5 +72,8 @@ fun NavigationUIZoomButton( @Preview @Composable fun NavigationUIZoomButtonPreview() { - Box(Modifier.background(Color.LightGray).padding(16.dp)) { NavigationUIZoomButton({}, {}) } + Box(Modifier.background(Color.LightGray).padding(16.dp)) { + NavigationUIZoomButton( + buttonSize = DpSize(56.dp, 56.dp), onClickZoomIn = {}, onClickZoomOut = {}) + } } diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/NavigatingInnerGridView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/NavigatingInnerGridView.kt index aad7f8cb..a7acfbe0 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/NavigatingInnerGridView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/NavigatingInnerGridView.kt @@ -1,5 +1,7 @@ package com.stadiamaps.ferrostar.composeui.views.gridviews +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.width @@ -7,15 +9,18 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.VolumeOff import androidx.compose.material.icons.automirrored.filled.VolumeUp import androidx.compose.material.icons.filled.Navigation -import androidx.compose.material.icons.filled.VolumeOff +import androidx.compose.material.icons.filled.Route import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import com.stadiamaps.ferrostar.composeui.R +import com.stadiamaps.ferrostar.composeui.config.CameraControlState import com.stadiamaps.ferrostar.composeui.views.controls.NavigationUIButton import com.stadiamaps.ferrostar.composeui.views.controls.NavigationUIZoomButton @@ -25,11 +30,11 @@ fun NavigatingInnerGridView( showMute: Boolean = true, isMuted: Boolean?, onClickMute: () -> Unit = {}, + buttonSize: DpSize, + cameraControlState: CameraControlState = CameraControlState.Hidden, showZoom: Boolean = true, onClickZoomIn: () -> Unit = {}, onClickZoomOut: () -> Unit = {}, - showCentering: Boolean = true, - onClickCenter: () -> Unit = {}, topCenter: @Composable () -> Unit = { Spacer(Modifier.width(12.dp)) }, centerStart: @Composable () -> Unit = { Spacer(Modifier.width(12.dp)) }, bottomEnd: @Composable () -> Unit = { Spacer(Modifier.width(12.dp)) } @@ -41,29 +46,52 @@ fun NavigatingInnerGridView( }, topCenter = topCenter, topEnd = { - if (showMute && isMuted != null) { - NavigationUIButton(onClick = onClickMute) { - if (isMuted) { - Icon( - Icons.AutoMirrored.Filled.VolumeOff, - contentDescription = stringResource(id = R.string.unmute_description)) - } else { - Icon( - Icons.AutoMirrored.Filled.VolumeUp, - contentDescription = stringResource(id = R.string.mute_description)) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + when (cameraControlState) { + CameraControlState.Hidden -> { + // Nothing to draw here :) + } + is CameraControlState.ShowRecenter -> { + // We decided to put this in the bottom corner for now + } + is CameraControlState.ShowRouteOverview -> { + NavigationUIButton( + onClick = cameraControlState.updateCamera, buttonSize = buttonSize) { + Icon( + Icons.Default.Route, + modifier = Modifier.rotate(90.0f), + contentDescription = stringResource(id = R.string.route_overview)) + } + } + } + + // NOTE: Some controls hidden when the camera is not following the user + if (showMute && + isMuted != null && + cameraControlState !is CameraControlState.ShowRecenter) { + NavigationUIButton(onClick = onClickMute, buttonSize = buttonSize) { + if (isMuted) { + Icon( + Icons.AutoMirrored.Filled.VolumeOff, + contentDescription = stringResource(id = R.string.unmute_description)) + } else { + Icon( + Icons.AutoMirrored.Filled.VolumeUp, + contentDescription = stringResource(id = R.string.mute_description)) + } } } } }, centerStart = centerStart, centerEnd = { - if (showZoom) { - NavigationUIZoomButton(onClickZoomIn, onClickZoomOut) + if (showZoom && cameraControlState !is CameraControlState.ShowRecenter) { + NavigationUIZoomButton(buttonSize, onClickZoomIn, onClickZoomOut) } }, bottomStart = { - if (showCentering) { - NavigationUIButton(onClick = onClickCenter) { + if (cameraControlState is CameraControlState.ShowRecenter) { + NavigationUIButton(onClick = cameraControlState.updateCamera, buttonSize = buttonSize) { Icon( Icons.Filled.Navigation, contentDescription = stringResource(id = R.string.recenter)) @@ -75,14 +103,56 @@ fun NavigatingInnerGridView( @Preview(device = Devices.PIXEL_5) @Composable -fun NavigatingInnerGridViewPreview() { - NavigatingInnerGridView(modifier = Modifier.fillMaxSize(), isMuted = false) +fun NavigatingInnerGridViewNonTrackingPreview() { + NavigatingInnerGridView( + modifier = Modifier.fillMaxSize(), + isMuted = false, + buttonSize = DpSize(56.dp, 56.dp), + cameraControlState = + CameraControlState.ShowRecenter { + // Do nothing + }) +} + +@Preview(device = Devices.PIXEL_5) +@Composable +fun NavigatingInnerGridViewTrackingPreview() { + NavigatingInnerGridView( + modifier = Modifier.fillMaxSize(), + isMuted = false, + buttonSize = DpSize(56.dp, 56.dp), + cameraControlState = + CameraControlState.ShowRouteOverview { + // Do nothing + }) +} + +@Preview( + device = + "spec:width=411dp,height=891dp,dpi=420,isRound=false,chinSize=0dp,orientation=landscape") +@Composable +fun NavigatingInnerGridViewLandscapeNonTrackingPreview() { + NavigatingInnerGridView( + modifier = Modifier.fillMaxSize(), + isMuted = true, + buttonSize = DpSize(56.dp, 56.dp), + cameraControlState = + CameraControlState.ShowRecenter { + // Do nothing + }) } @Preview( device = "spec:width=411dp,height=891dp,dpi=420,isRound=false,chinSize=0dp,orientation=landscape") @Composable -fun NavigatingInnerGridViewLandscapePreview() { - NavigatingInnerGridView(modifier = Modifier.fillMaxSize(), isMuted = true) +fun NavigatingInnerGridViewLandscapeTrackingPreview() { + NavigatingInnerGridView( + modifier = Modifier.fillMaxSize(), + isMuted = true, + buttonSize = DpSize(56.dp, 56.dp), + cameraControlState = + CameraControlState.ShowRouteOverview { + // Do nothing + }) } diff --git a/android/composeui/src/main/res/values/strings.xml b/android/composeui/src/main/res/values/strings.xml index 975da209..3112c6e6 100644 --- a/android/composeui/src/main/res/values/strings.xml +++ b/android/composeui/src/main/res/values/strings.xml @@ -17,4 +17,5 @@ Preparing... Arrived You have arrived at your destination. + Route Overview \ No newline at end of file diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigatingInnerGridViewTest.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigatingInnerGridViewTest.kt index 5008ade9..d4ed607c 100644 --- a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigatingInnerGridViewTest.kt +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigatingInnerGridViewTest.kt @@ -1,7 +1,9 @@ package com.stadiamaps.ferrostar.views -import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridViewLandscapePreview -import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridViewPreview +import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridViewLandscapeNonTrackingPreview +import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridViewLandscapeTrackingPreview +import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridViewNonTrackingPreview +import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridViewTrackingPreview import com.stadiamaps.ferrostar.support.paparazziDefault import com.stadiamaps.ferrostar.support.withSnapshotBackground import org.junit.Rule @@ -12,12 +14,26 @@ class NavigatingInnerGridViewTest { @get:Rule val paparazzi = paparazziDefault() @Test - fun testNavigatingInnerGridView() { - paparazzi.snapshot { withSnapshotBackground { NavigatingInnerGridViewPreview() } } + fun testNavigatingInnerGridViewTracking() { + paparazzi.snapshot { withSnapshotBackground { NavigatingInnerGridViewTrackingPreview() } } } @Test - fun testNavigatingInnerGridViewLandscape() { - paparazzi.snapshot { withSnapshotBackground { NavigatingInnerGridViewLandscapePreview() } } + fun testNavigatingInnerGridViewNonTracking() { + paparazzi.snapshot { withSnapshotBackground { NavigatingInnerGridViewNonTrackingPreview() } } + } + + @Test + fun testNavigatingInnerGridViewTrackingLandscape() { + paparazzi.snapshot { + withSnapshotBackground { NavigatingInnerGridViewLandscapeTrackingPreview() } + } + } + + @Test + fun testNavigatingInnerGridViewNonTrackingLandscape() { + paparazzi.snapshot { + withSnapshotBackground { NavigatingInnerGridViewLandscapeNonTrackingPreview() } + } } } diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigationUIButtonTest.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigationUIButtonTest.kt index f576a684..ca2669c1 100644 --- a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigationUIButtonTest.kt +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigationUIButtonTest.kt @@ -1,14 +1,14 @@ package com.stadiamaps.ferrostar.views import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Adb import androidx.compose.material3.Icon import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import com.stadiamaps.ferrostar.composeui.views.controls.NavigationUIButton import com.stadiamaps.ferrostar.composeui.views.controls.NavigationUIZoomButton @@ -20,11 +20,13 @@ class NavigationUIButtonTest { @get:Rule val paparazzi = paparazziDefault() + private val buttonSize = DpSize(56.dp, 56.dp) + @Test fun testNavigationUIButton() { paparazzi.snapshot { - Box(modifier = Modifier.width(56.dp).height(56.dp).padding(16.dp)) { - NavigationUIButton(onClick = { /* no action */ }) { + Box(modifier = Modifier.size(buttonSize).padding(16.dp)) { + NavigationUIButton(onClick = { /* no action */ }, buttonSize = buttonSize) { Icon(Icons.Filled.Adb, contentDescription = "ADB") } } @@ -34,9 +36,10 @@ class NavigationUIButtonTest { @Test fun testNavigationUIButtonCustomized() { paparazzi.snapshot { - Box(modifier = Modifier.width(56.dp).height(56.dp).padding(16.dp)) { + Box(modifier = Modifier.size(buttonSize).padding(16.dp)) { NavigationUIButton( onClick = { /* no action */ }, + buttonSize = buttonSize, containerColor = Color.Black, contentColor = Color.White) { Icon(Icons.Filled.Adb, contentDescription = "ADB") @@ -48,9 +51,11 @@ class NavigationUIButtonTest { @Test fun testNavigationUIZoomButton() { paparazzi.snapshot { - Box(modifier = Modifier.width(56.dp).height(56.dp).padding(16.dp)) { + Box(modifier = Modifier.size(buttonSize).padding(16.dp)) { NavigationUIZoomButton( - onClickZoomIn = { /* no action */ }, onClickZoomOut = { /* no action */ }) + onClickZoomIn = { /* no action */ }, + onClickZoomOut = { /* no action */ }, + buttonSize = buttonSize) } } } @@ -58,8 +63,9 @@ class NavigationUIButtonTest { @Test fun testNavigationUIZoomButtonCustomized() { paparazzi.snapshot { - Box(modifier = Modifier.width(56.dp).height(56.dp).padding(16.dp)) { + Box(modifier = Modifier.size(buttonSize).padding(16.dp)) { NavigationUIZoomButton( + buttonSize = buttonSize, onClickZoomIn = { /* no action */ }, onClickZoomOut = { /* no action */ }, containerColor = Color.Black, diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridView.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridView.png deleted file mode 100644 index fbd51cda..00000000 Binary files a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridView.png and /dev/null differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewLandscape.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewLandscape.png deleted file mode 100644 index 6131454d..00000000 Binary files a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewLandscape.png and /dev/null differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewNonTracking.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewNonTracking.png new file mode 100644 index 00000000..6a3212e4 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewNonTracking.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewNonTrackingLandscape.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewNonTrackingLandscape.png new file mode 100644 index 00000000..6a3212e4 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewNonTrackingLandscape.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewTracking.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewTracking.png new file mode 100644 index 00000000..82e5a057 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewTracking.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewTrackingLandscape.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewTrackingLandscape.png new file mode 100644 index 00000000..db8afdc0 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewTrackingLandscape.png differ diff --git a/android/core/build.gradle b/android/core/build.gradle index 2c948171..44416701 100644 --- a/android/core/build.gradle +++ b/android/core/build.gradle @@ -12,12 +12,12 @@ plugins { android { namespace 'com.stadiamaps.ferrostar.core' - compileSdk 34 + compileSdk 35 ndkVersion "26.2.11394342" defaultConfig { minSdk 25 - targetSdk 34 + targetSdk 35 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Extensions.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Extensions.kt index c5f73a17..71d66f2d 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Extensions.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Extensions.kt @@ -1,7 +1,10 @@ package com.stadiamaps.ferrostar.core +import kotlin.math.max +import kotlin.math.min import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import uniffi.ferrostar.GeographicCoordinate import uniffi.ferrostar.Route import uniffi.ferrostar.RouteRequest import uniffi.ferrostar.getRoutePolyline @@ -25,3 +28,26 @@ fun RouteRequest.toOkhttp3Request(): Request { .apply { headers.map { (name, value) -> header(name, value) } } .build() } + +/** A neutral bounding box type, which is not dependent on any particular map library. */ +data class BoundingBox( + val north: Double, + val east: Double, + val south: Double, + val west: Double, +) + +fun List.boundingBox(): BoundingBox? = + this.firstOrNull()?.let { start -> + val initial = + BoundingBox(north = start.lat, east = start.lng, south = start.lat, west = start.lng) + + fold(initial) { acc, current -> + BoundingBox( + north = max(acc.north, current.lat), + east = max(acc.east, current.lng), + south = min(acc.south, current.lat), + west = min(acc.west, current.lng), + ) + } + } 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 8c80ea32..4aa40e84 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 @@ -3,11 +3,7 @@ package com.stadiamaps.ferrostar.core import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapter -import com.stadiamaps.ferrostar.core.annotation.valhalla.valhallaExtendedOSRMAnnotationPublisher import com.stadiamaps.ferrostar.core.service.ForegroundServiceManager -import java.net.URL -import java.time.Instant -import java.util.concurrent.Executors import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -27,6 +23,9 @@ import uniffi.ferrostar.TripState import uniffi.ferrostar.UserLocation import uniffi.ferrostar.Uuid import uniffi.ferrostar.Waypoint +import java.net.URL +import java.time.Instant +import java.util.concurrent.Executors /** Represents the complete state of the navigation session provided by FerrostarCore-RS. */ data class NavigationState( @@ -39,6 +38,13 @@ data class NavigationState( companion object } +fun NavigationState.isNavigating(): Boolean = + when (tripState) { + TripState.Complete, + TripState.Idle -> false + is TripState.Navigating -> true + } + private val moshi: Moshi = Moshi.Builder().build() @OptIn(ExperimentalStdlibApi::class) private val jsonAdapter: JsonAdapter> = moshi.adapter>() @@ -214,15 +220,10 @@ class FerrostarCore( * @param route the route to navigate. * @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. */ @Throws(UserLocationUnknown::class) - fun startNavigation( - route: Route, - config: NavigationControllerConfig? = null - ): DefaultNavigationViewModel { + fun startNavigation(route: Route, config: NavigationControllerConfig? = null) { stopNavigation() // Start the foreground notification service @@ -248,12 +249,6 @@ class FerrostarCore( _state.value = newState locationProvider.addListener(this, _executor) - - return DefaultNavigationViewModel( - this, - spokenInstructionObserver, - locationProvider, - valhallaExtendedOSRMAnnotationPublisher()) } /** @@ -304,9 +299,11 @@ class FerrostarCore( } } - fun stopNavigation() { + fun stopNavigation(stopLocationUpdates: Boolean = true) { foregroundServiceManager?.stopService() - locationProvider.removeListener(this) + if (stopLocationUpdates) { + locationProvider.removeListener(this) + } _navigationController?.destroy() _navigationController = null _state.value = NavigationState() 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 5bd011cd..7dbd9121 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 @@ -81,6 +81,8 @@ data class NavigationUiState( currentStepRoadName = coreState.tripState.currentRoadName(), remainingSteps = coreState.tripState.remainingSteps()) } + + fun isNavigating(): Boolean = progress != null } interface NavigationViewModel { @@ -88,36 +90,39 @@ interface NavigationViewModel { fun toggleMute() - fun stopNavigation() - - fun isNavigating(): Boolean = uiState.value.progress != null + fun stopNavigation(stopLocationUpdates: Boolean = true) // TODO: We think the camera may eventually need to be owned by the view model, but that's going // to be a very big refactor (maybe even crossing into the MapLibre Compose project) } +/** + * A basic implementation of a navigation view model. + * + * This is sufficient for simple applications, particularly those which only present maps and + * navigation for part of the app lifecycle. Apps which revolve around a single map-centric + * interface that is reused across navigation sessions will probably need to craft their own view + * model. + */ class DefaultNavigationViewModel( private val ferrostarCore: FerrostarCore, - private val spokenInstructionObserver: SpokenInstructionObserver? = null, - private val locationProvider: LocationProvider, private val annotationPublisher: AnnotationPublisher<*> = NoOpAnnotationPublisher() ) : ViewModel(), NavigationViewModel { - private var userLocation: UserLocation? = locationProvider.lastLocation private val muteState: StateFlow = - spokenInstructionObserver?.muteState ?: MutableStateFlow(null) + ferrostarCore.spokenInstructionObserver?.muteState ?: MutableStateFlow(null) override val uiState = combine(ferrostarCore.state, muteState) { a, b -> a to b } .map { (coreState, muteState) -> annotationPublisher.map(coreState) to muteState } .map { (stateWrapper, muteState) -> val coreState = stateWrapper.state - val location = locationProvider.lastLocation - userLocation = + val location = ferrostarCore.locationProvider.lastLocation + val userLocation = when (coreState.tripState) { is TripState.Navigating -> coreState.tripState.snappedUserLocation is TripState.Complete, - TripState.Idle -> locationProvider.lastLocation + TripState.Idle -> ferrostarCore.locationProvider.lastLocation } uiState(coreState, muteState, location, userLocation) // This awkward dance is required because Kotlin doesn't have a way to map over @@ -130,15 +135,16 @@ class DefaultNavigationViewModel( initialValue = uiState( ferrostarCore.state.value, - spokenInstructionObserver?.isMuted, - locationProvider.lastLocation, - userLocation)) + ferrostarCore.spokenInstructionObserver?.isMuted, + ferrostarCore.locationProvider.lastLocation, + ferrostarCore.locationProvider.lastLocation)) - override fun stopNavigation() { - ferrostarCore.stopNavigation() + override fun stopNavigation(stopLocationUpdates: Boolean) { + ferrostarCore.stopNavigation(stopLocationUpdates = stopLocationUpdates) } override fun toggleMute() { + val spokenInstructionObserver = ferrostarCore.spokenInstructionObserver if (spokenInstructionObserver == null) { Log.d("NavigationViewModel", "Spoken instruction observer is null, mute operation ignored.") return 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 35332611..1f2b1d20 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 @@ -73,5 +73,5 @@ class MockNavigationViewModel(override val uiState: StateFlow ViewModel(), NavigationViewModel { override fun toggleMute() {} - override fun stopNavigation() {} + override fun stopNavigation(stopLocationUpdates: Boolean) {} } diff --git a/android/demo-app/build.gradle b/android/demo-app/build.gradle index ffe78c66..b6a00a01 100644 --- a/android/demo-app/build.gradle +++ b/android/demo-app/build.gradle @@ -7,7 +7,7 @@ plugins { android { namespace 'com.stadiamaps.ferrostar' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.stadiamaps.ferrostar.demo" 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 d583eb12..caf89530 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 @@ -125,4 +125,6 @@ object AppModule { // The AndroidTtsObserver handles spoken instructions as they are triggered by FerrostarCore. val ttsObserver: AndroidTtsObserver by lazy { AndroidTtsObserver(appContext) } + + val viewModel: DemoNavigationViewModel by lazy { DemoNavigationViewModel() } } 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 54a80cc6..5a5aa259 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 @@ -13,8 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -29,12 +27,10 @@ import com.stadiamaps.ferrostar.composeui.runtime.KeepScreenOnDisposableEffect import com.stadiamaps.ferrostar.composeui.views.gridviews.InnerGridView import com.stadiamaps.ferrostar.core.AndroidSystemLocationProvider import com.stadiamaps.ferrostar.core.LocationProvider -import com.stadiamaps.ferrostar.core.NavigationViewModel import com.stadiamaps.ferrostar.core.SimulatedLocationProvider import com.stadiamaps.ferrostar.core.toAndroidLocation import com.stadiamaps.ferrostar.googleplayservices.FusedLocationProvider import com.stadiamaps.ferrostar.maplibreui.views.DynamicallyOrientingNavigationView -import java.util.concurrent.Executors import kotlin.math.min import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -46,16 +42,11 @@ import uniffi.ferrostar.WaypointKind fun DemoNavigationScene( savedInstanceState: Bundle?, locationProvider: LocationProvider = AppModule.locationProvider, + viewModel: DemoNavigationViewModel = AppModule.viewModel ) { - val executor = remember { Executors.newSingleThreadScheduledExecutor() } - // Keeps the screen on at consistent brightness while this Composable is in the view hierarchy. KeepScreenOnDisposableEffect() - // NOTE: We are aware that this is not a particularly great pattern. - // We are working on improving this. See the discussion on - // https://github.com/stadiamaps/ferrostar/pull/295. - var viewModel by remember { mutableStateOf(DemoNavigationViewModel()) } val scope = rememberCoroutineScope() // Get location permissions. @@ -73,7 +64,7 @@ fun DemoNavigationScene( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) } - val vmState = viewModel.uiState.collectAsState(scope.coroutineContext) + val vmState by viewModel.uiState.collectAsState(scope.coroutineContext) val permissionsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { @@ -82,7 +73,7 @@ fun DemoNavigationScene( permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { val vm = viewModel if ((locationProvider is AndroidSystemLocationProvider || - locationProvider is FusedLocationProvider) && vm is DemoNavigationViewModel) { + locationProvider is FusedLocationProvider)) { // Activate location updates in the view model vm.startLocationUpdates(locationProvider) } @@ -104,7 +95,7 @@ fun DemoNavigationScene( } // For smart casting - val loc = vmState.value.location + val loc = vmState.location if (loc == null) { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Text("Waiting to acquire your GPS location...", modifier = Modifier.padding(innerPadding)) @@ -122,15 +113,9 @@ fun DemoNavigationScene( // Snapping works well for most motor vehicle navigation. // Other travel modes though, such as walking, may not want snapping. snapUserLocationToRoute = false, - onTapExit = { - viewModel.stopNavigation() - val vm = DemoNavigationViewModel() - viewModel = vm - - vm.startLocationUpdates(locationProvider) - }, + onTapExit = { viewModel.stopNavigation() }, userContent = { modifier -> - if (!viewModel.isNavigating()) { + if (!vmState.isNavigating()) { InnerGridView( modifier = modifier.fillMaxSize().padding(bottom = 16.dp, top = 16.dp), topCenter = { @@ -152,7 +137,7 @@ fun DemoNavigationScene( )) val route = routes.first() - viewModel = AppModule.ferrostarCore.startNavigation(route = route) + AppModule.ferrostarCore.startNavigation(route = route) if (locationProvider is SimulatedLocationProvider) { locationProvider.setSimulatedRoute(route) @@ -167,7 +152,7 @@ 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.) - uiState.value.location?.let { location -> + uiState.location?.let { location -> Circle( center = LatLng(location.coordinates.lat, location.coordinates.lng), radius = 10f, diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt index 40d59d91..84e6e2c2 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt @@ -1,48 +1,64 @@ package com.stadiamaps.ferrostar +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.stadiamaps.ferrostar.core.FerrostarCore import com.stadiamaps.ferrostar.core.LocationProvider import com.stadiamaps.ferrostar.core.LocationUpdateListener import com.stadiamaps.ferrostar.core.NavigationUiState import com.stadiamaps.ferrostar.core.NavigationViewModel +import com.stadiamaps.ferrostar.core.isNavigating import java.util.concurrent.Executors import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import uniffi.ferrostar.Heading +import uniffi.ferrostar.TripState import uniffi.ferrostar.UserLocation -// NOTE: We are aware that this is not a particularly great ViewModel. -// We are working on improving this. See the discussion on -// https://github.com/stadiamaps/ferrostar/pull/295. -class DemoNavigationViewModel : ViewModel(), NavigationViewModel { +class DemoNavigationViewModel( + // This is a simple example, but these would typically be dependency injected + private val ferrostarCore: FerrostarCore = AppModule.ferrostarCore, +) : ViewModel(), LocationUpdateListener, NavigationViewModel { private val locationStateFlow = MutableStateFlow(null) private val executor = Executors.newSingleThreadScheduledExecutor() + private val muteState: StateFlow = + ferrostarCore.spokenInstructionObserver?.muteState ?: MutableStateFlow(null) + fun startLocationUpdates(locationProvider: LocationProvider) { locationStateFlow.update { locationProvider.lastLocation } - locationProvider.addListener( - object : LocationUpdateListener { - override fun onLocationUpdated(location: UserLocation) { - locationStateFlow.update { location } - } + locationProvider.addListener(this, executor) + } - override fun onHeadingUpdated(heading: Heading) { - // TODO: Heading - } - }, - executor) + fun stopLocationUpdates(locationProvider: LocationProvider) { + locationProvider.removeListener(this) } - override val uiState = - locationStateFlow - .map { userLocation -> - // TODO: Heading - NavigationUiState( - userLocation, null, null, null, null, null, null, false, null, null, null, null) + override val uiState: StateFlow = + combine(ferrostarCore.state, muteState, locationStateFlow) { a, b, c -> Triple(a, b, c) } + .map { (ferrostarCoreState, isMuted, userLocation) -> + if (ferrostarCoreState.isNavigating()) { + val tripState = ferrostarCoreState.tripState + val location = ferrostarCore.locationProvider.lastLocation + val snappedLocation = + when (tripState) { + is TripState.Navigating -> tripState.snappedUserLocation + is TripState.Complete, + TripState.Idle -> ferrostarCore.locationProvider.lastLocation + } + NavigationUiState.fromFerrostar( + ferrostarCoreState, isMuted, location, snappedLocation) + } else { + // TODO: Heading + NavigationUiState( + userLocation, null, null, null, null, null, null, false, null, null, null, null) + } } .stateIn( scope = viewModelScope, @@ -53,10 +69,23 @@ class DemoNavigationViewModel : ViewModel(), NavigationViewModel { null, null, null, null, null, null, null, false, null, null, null, null)) override fun toggleMute() { - // Do nothing + val spokenInstructionObserver = ferrostarCore.spokenInstructionObserver + if (spokenInstructionObserver == null) { + Log.d("NavigationViewModel", "Spoken instruction observer is null, mute operation ignored.") + return + } + spokenInstructionObserver.setMuted(!spokenInstructionObserver.isMuted) + } + + override fun stopNavigation(stopLocationUpdates: Boolean) { + ferrostarCore.stopNavigation(stopLocationUpdates = stopLocationUpdates) + } + + override fun onLocationUpdated(location: UserLocation) { + locationStateFlow.update { location } } - override fun stopNavigation() { - // Do nothing + override fun onHeadingUpdated(heading: Heading) { + // TODO: Heading } } diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt index 1d039aa4..47c26cf3 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt @@ -7,8 +7,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.material3.Surface -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import com.stadiamaps.ferrostar.core.AndroidTtsStatusListener import com.stadiamaps.ferrostar.ui.theme.FerrostarTheme import java.util.Locale diff --git a/android/google-play-services/build.gradle b/android/google-play-services/build.gradle index 17c765e6..4be1751f 100644 --- a/android/google-play-services/build.gradle +++ b/android/google-play-services/build.gradle @@ -10,7 +10,7 @@ plugins { android { namespace 'com.stadiamaps.ferrostar.googleplayservices' - compileSdk 34 + compileSdk 35 defaultConfig { minSdk 25 diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 2deebe99..6b5df06d 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -14,7 +14,7 @@ androidx-activity-compose = "1.9.3" compose = "2024.10.00" okhttp = "4.12.0" moshi = "1.15.1" -maplibre-compose = "0.2.3" +maplibre-compose = "0.3.0" playServicesLocation = "21.3.0" junit = "4.13.2" junitVersion = "1.2.1" diff --git a/android/maplibreui/build.gradle b/android/maplibreui/build.gradle index 0dd79f3b..e16e46fa 100644 --- a/android/maplibreui/build.gradle +++ b/android/maplibreui/build.gradle @@ -12,7 +12,7 @@ plugins { android { namespace 'com.stadiamaps.ferrostar.maplibreui' - compileSdk 34 + compileSdk 35 defaultConfig { minSdk 25 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 9954d4cd..6b8e6990 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 @@ -4,8 +4,10 @@ import androidx.compose.foundation.layout.fillMaxSize 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.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.maps.Style @@ -16,7 +18,6 @@ import com.maplibre.compose.ramani.LocationRequestProperties import com.maplibre.compose.ramani.MapLibreComposable import com.maplibre.compose.settings.MapControls import com.stadiamaps.ferrostar.core.NavigationUiState -import com.stadiamaps.ferrostar.core.NavigationViewModel import com.stadiamaps.ferrostar.core.toAndroidLocation import com.stadiamaps.ferrostar.maplibreui.extensions.NavigationDefault import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera @@ -30,13 +31,13 @@ import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera * approach and later verified that Google Maps does the same thing in their compose SDK. * @param navigationCamera The default camera settings to use when navigation starts. This will be * re-applied to the camera any time that navigation is started. - * @param viewModel The navigation view model provided by Ferrostar Core. + * @param uiState The navigation UI state. * @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! + * If unspecified, the camera will change to `navigationCamera` if navigation is in progress. * @param content Any additional composable map symbol content to render. */ @Composable @@ -44,23 +45,17 @@ fun NavigationMapView( styleUrl: String, camera: MutableState, navigationCamera: MapViewCamera = navigationMapViewCamera(), - viewModel: NavigationViewModel, + uiState: NavigationUiState, mapControls: State, locationRequestProperties: LocationRequestProperties = LocationRequestProperties.NavigationDefault(), snapUserLocationToRoute: Boolean = true, - onMapReadyCallback: (Style) -> Unit = { - if (viewModel.isNavigating()) camera.value = navigationCamera - }, - content: @Composable @MapLibreComposable ((State) -> Unit)? = null + onMapReadyCallback: ((Style) -> Unit)? = null, + content: @Composable @MapLibreComposable ((NavigationUiState) -> Unit)? = null ) { - val uiState = viewModel.uiState.collectAsState() - - // TODO: This works for now, but in the end, the view model may need to "own" the camera. - // We can move this code if we do such a refactor. - var isNavigating = remember { viewModel.isNavigating() } - if (viewModel.isNavigating() != isNavigating) { - isNavigating = viewModel.isNavigating() + var isNavigating by remember { mutableStateOf(uiState.isNavigating()) } + if (uiState.isNavigating() != isNavigating) { + isNavigating = uiState.isNavigating() if (isNavigating) { camera.value = navigationCamera @@ -69,12 +64,10 @@ fun NavigationMapView( val locationEngine = remember { StaticLocationEngine() } locationEngine.lastLocation = - uiState.value.let { state -> - if (snapUserLocationToRoute) { - state.snappedLocation?.toAndroidLocation() - } else { - state.location?.toAndroidLocation() - } + if (snapUserLocationToRoute) { + uiState.snappedLocation?.toAndroidLocation() + } else { + uiState.location?.toAndroidLocation() } MapView( @@ -84,9 +77,10 @@ fun NavigationMapView( mapControls, locationRequestProperties = locationRequestProperties, locationEngine = locationEngine, - onMapReadyCallback = onMapReadyCallback, + onMapReadyCallback = + onMapReadyCallback ?: { if (isNavigating) camera.value = navigationCamera }, ) { - val geometry = uiState.value.routeGeometry + val geometry = uiState.routeGeometry if (geometry != null) BorderedPolyline(points = geometry.map { LatLng(it.lat, it.lng) }, zIndex = 0) diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationViewMetrics.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationViewMetrics.kt new file mode 100644 index 00000000..f9a55802 --- /dev/null +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationViewMetrics.kt @@ -0,0 +1,8 @@ +package com.stadiamaps.ferrostar.maplibreui + +import androidx.compose.ui.unit.DpSize + +data class NavigationViewMetrics( + val progressViewSize: DpSize, + val instructionsViewSize: DpSize, +) diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfig.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfig.kt deleted file mode 100644 index a59f8af6..00000000 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfig.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.stadiamaps.ferrostar.maplibreui.config - -data class VisualNavigationViewConfig( - var showMute: Boolean = false, - var showZoom: Boolean = false -) { - companion object { - fun Default() = VisualNavigationViewConfig(showMute = true, showZoom = true) - } -} - -/** Enables the mute button in the navigation view. */ -fun VisualNavigationViewConfig.useMuteButton(): VisualNavigationViewConfig { - showMute = true - return this -} - -/** Enables the zoom button in the navigation view. */ -fun VisualNavigationViewConfig.useZoomButton(): VisualNavigationViewConfig { - showZoom = true - return this -} diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/extensions/VisualNavigationViewConfig.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/extensions/VisualNavigationViewConfig.kt new file mode 100644 index 00000000..0821f916 --- /dev/null +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/extensions/VisualNavigationViewConfig.kt @@ -0,0 +1,65 @@ +package com.stadiamaps.ferrostar.maplibreui.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection +import com.mapbox.mapboxsdk.geometry.LatLngBounds +import com.maplibre.compose.camera.CameraState +import com.maplibre.compose.camera.MapViewCamera +import com.maplibre.compose.camera.models.CameraPadding +import com.stadiamaps.ferrostar.composeui.config.CameraControlState +import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig +import com.stadiamaps.ferrostar.core.NavigationUiState +import com.stadiamaps.ferrostar.core.boundingBox +import com.stadiamaps.ferrostar.maplibreui.NavigationViewMetrics + +@Composable +fun VisualNavigationViewConfig.cameraControlState( + camera: MutableState, + navigationCamera: MapViewCamera, + uiState: NavigationUiState, + navigationViewMetrics: NavigationViewMetrics +): CameraControlState { + val cameraIsTrackingLocation = camera.value.state is CameraState.TrackingUserLocationWithBearing + val cameraControlState = + if (!cameraIsTrackingLocation) { + CameraControlState.ShowRecenter { camera.value = navigationCamera } + } else { + val bbox = uiState.routeGeometry?.boundingBox() + if (bbox != null) { + val scale = LocalDensity.current.density + val progressViewHeight = navigationViewMetrics.progressViewSize.height.value.toDouble() + val instructionsViewHeight = + navigationViewMetrics.instructionsViewSize.height.value.toDouble() + val layoutDirection = LocalLayoutDirection.current + + // Bottom padding must take the recenter button into account + val bottomPadding = (progressViewHeight + this.buttonSize.height.value + 50) * scale + // The top padding needs to take the puck into account + val topPadding = (instructionsViewHeight + 75) * scale + val (startPadding, endPadding) = + when (layoutDirection) { + LayoutDirection.Ltr -> 20.0 * scale to (this.buttonSize.width.value + 50) * scale + + LayoutDirection.Rtl -> (this.buttonSize.width.value + 50) * scale to 20.0 * scale + } + + CameraControlState.ShowRouteOverview { + camera.value = + MapViewCamera.BoundingBox( + LatLngBounds.from(bbox.north, bbox.east, bbox.south, bbox.west), + padding = + CameraPadding( + startPadding.toDouble(), + topPadding, + endPadding.toDouble(), + bottomPadding)) + } + } else { + CameraControlState.Hidden + } + } + return cameraControlState +} diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt index e9a37c82..b51f9e42 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt @@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding 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.mutableStateOf import androidx.compose.runtime.remember @@ -23,12 +23,12 @@ import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.ramani.LocationRequestProperties import com.maplibre.compose.ramani.MapLibreComposable import com.maplibre.compose.rememberSaveableMapViewCamera +import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.composeui.runtime.paddingForGridView import com.stadiamaps.ferrostar.composeui.views.CurrentRoadNameView import com.stadiamaps.ferrostar.core.NavigationUiState import com.stadiamaps.ferrostar.core.NavigationViewModel import com.stadiamaps.ferrostar.maplibreui.NavigationMapView -import com.stadiamaps.ferrostar.maplibreui.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.maplibreui.extensions.NavigationDefault import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import com.stadiamaps.ferrostar.maplibreui.runtime.rememberMapControlsForProgressViewHeight @@ -75,13 +75,14 @@ fun DynamicallyOrientingNavigationView( }, onTapExit: (() -> Unit)? = null, userContent: @Composable (BoxScope.(Modifier) -> Unit)? = null, - mapContent: @Composable @MapLibreComposable ((State) -> Unit)? = null, + mapContent: @Composable @MapLibreComposable ((NavigationUiState) -> Unit)? = null, ) { val orientation = LocalConfiguration.current.orientation - // Maintain the actual size of the progress view for MapControl layout purposes. + // Maintain the actual size of the progress view for dynamic layout purposes. val rememberProgressViewSize = remember { mutableStateOf(DpSize.Zero) } val progressViewSize by rememberProgressViewSize + val uiState by viewModel.uiState.collectAsState() // Get the correct padding based on edge-to-edge status. val gridPadding = paddingForGridView() @@ -91,28 +92,25 @@ fun DynamicallyOrientingNavigationView( Box(modifier) { NavigationMapView( - styleUrl, - camera, - navigationCamera, - viewModel, - mapControls, - locationRequestProperties, - snapUserLocationToRoute, - onMapReadyCallback = { - if (viewModel.isNavigating()) { - camera.value = navigationCamera - } - }, - mapContent) + styleUrl = styleUrl, + camera = camera, + navigationCamera = navigationCamera, + uiState = uiState, + mapControls = mapControls, + locationRequestProperties = locationRequestProperties, + snapUserLocationToRoute = snapUserLocationToRoute, + content = mapContent) - if (viewModel.isNavigating()) { + if (uiState.isNavigating()) { when (orientation) { Configuration.ORIENTATION_LANDSCAPE -> { LandscapeNavigationOverlayView( modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), camera = camera, + navigationCamera = navigationCamera, viewModel = viewModel, config = config, + progressViewSize = rememberProgressViewSize, onTapExit = onTapExit, currentRoadNameView = currentRoadNameView) } @@ -121,6 +119,7 @@ fun DynamicallyOrientingNavigationView( PortraitNavigationOverlayView( modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), camera = camera, + navigationCamera = navigationCamera, viewModel = viewModel, config = config, progressViewSize = rememberProgressViewSize, 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 90c13661..baf6d6ff 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 @@ -10,7 +10,8 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding 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.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -18,6 +19,7 @@ import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.ramani.LocationRequestProperties import com.maplibre.compose.ramani.MapLibreComposable import com.maplibre.compose.rememberSaveableMapViewCamera +import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.composeui.runtime.paddingForGridView import com.stadiamaps.ferrostar.composeui.views.CurrentRoadNameView import com.stadiamaps.ferrostar.core.NavigationUiState @@ -25,7 +27,6 @@ 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.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.maplibreui.extensions.NavigationDefault import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import com.stadiamaps.ferrostar.maplibreui.runtime.rememberMapControlsForProgressViewHeight @@ -70,8 +71,10 @@ fun LandscapeNavigationView( } }, onTapExit: (() -> Unit)? = null, - content: @Composable @MapLibreComposable() ((State) -> Unit)? = null, + content: @Composable @MapLibreComposable() ((NavigationUiState) -> Unit)? = null, ) { + val uiState by viewModel.uiState.collectAsState() + // Get the correct padding based on edge-to-edge status. val gridPadding = paddingForGridView() @@ -82,7 +85,7 @@ fun LandscapeNavigationView( styleUrl, camera, navigationCamera, - viewModel, + uiState, mapControls, locationRequestProperties, snapUserLocationToRoute, @@ -93,6 +96,7 @@ fun LandscapeNavigationView( modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), config = config, camera = camera, + navigationCamera = navigationCamera, viewModel = viewModel, onTapExit = onTapExit, currentRoadNameView = currentRoadNameView) 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 8738fb5d..f7ba7ff5 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 @@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding 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.mutableStateOf import androidx.compose.runtime.remember @@ -23,6 +23,7 @@ import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.ramani.LocationRequestProperties import com.maplibre.compose.ramani.MapLibreComposable import com.maplibre.compose.rememberSaveableMapViewCamera +import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.composeui.runtime.paddingForGridView import com.stadiamaps.ferrostar.composeui.views.CurrentRoadNameView import com.stadiamaps.ferrostar.core.NavigationUiState @@ -30,7 +31,6 @@ 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.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.maplibreui.extensions.NavigationDefault import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import com.stadiamaps.ferrostar.maplibreui.runtime.rememberMapControlsForProgressViewHeight @@ -74,8 +74,10 @@ fun PortraitNavigationView( } }, onTapExit: (() -> Unit)? = null, - content: @Composable @MapLibreComposable() ((State) -> Unit)? = null, + content: @Composable @MapLibreComposable() ((NavigationUiState) -> Unit)? = null, ) { + val uiState by viewModel.uiState.collectAsState() + // Get the correct padding based on edge-to-edge status. val gridPadding = paddingForGridView() @@ -91,21 +93,24 @@ fun PortraitNavigationView( styleUrl, camera, navigationCamera, - viewModel, + uiState, mapControls, locationRequestProperties, snapUserLocationToRoute, onMapReadyCallback = { camera.value = navigationCamera }, content) - PortraitNavigationOverlayView( - modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), - config = config, - camera = camera, - viewModel = viewModel, - progressViewSize = rememberProgressViewSize, - onTapExit = onTapExit, - currentRoadNameView = currentRoadNameView) + if (uiState.isNavigating()) { + PortraitNavigationOverlayView( + modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), + config = config, + camera = camera, + navigationCamera = navigationCamera, + viewModel = viewModel, + progressViewSize = rememberProgressViewSize, + onTapExit = onTapExit, + currentRoadNameView = currentRoadNameView) + } } } @@ -113,8 +118,7 @@ fun PortraitNavigationView( @Composable private fun PortraitNavigationViewPreview() { val viewModel = - MockNavigationViewModel( - MutableStateFlow(NavigationUiState.pedestrianExample()).asStateFlow()) + MockNavigationViewModel(MutableStateFlow(NavigationUiState.pedestrianExample()).asStateFlow()) PortraitNavigationView( Modifier.fillMaxSize(), diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/LandscapeNavigationOverlayView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/LandscapeNavigationOverlayView.kt index 790f144e..a38fcb58 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/LandscapeNavigationOverlayView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/LandscapeNavigationOverlayView.kt @@ -12,13 +12,19 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import com.maplibre.compose.camera.CameraState import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.camera.extensions.incrementZoom import com.maplibre.compose.rememberSaveableMapViewCamera +import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.composeui.views.CurrentRoadNameView import com.stadiamaps.ferrostar.composeui.views.InstructionsView import com.stadiamaps.ferrostar.composeui.views.TripProgressView @@ -27,7 +33,8 @@ 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.config.VisualNavigationViewConfig +import com.stadiamaps.ferrostar.maplibreui.NavigationViewMetrics +import com.stadiamaps.ferrostar.maplibreui.extensions.cameraControlState import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -36,9 +43,10 @@ import kotlinx.coroutines.flow.asStateFlow fun LandscapeNavigationOverlayView( modifier: Modifier, camera: MutableState, - navigationCamera: MapViewCamera = navigationMapViewCamera(), + navigationCamera: MapViewCamera, viewModel: NavigationViewModel, config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(), + progressViewSize: MutableState = remember { mutableStateOf(DpSize.Zero) }, currentRoadNameView: @Composable (String?) -> Unit = { roadName -> if (roadName != null) { CurrentRoadNameView(roadName) @@ -47,14 +55,19 @@ fun LandscapeNavigationOverlayView( }, onTapExit: (() -> Unit)? = null, ) { + val density = LocalDensity.current val uiState by viewModel.uiState.collectAsState() - val cameraIsTrackingLocation = camera.value.state is CameraState.TrackingUserLocationWithBearing + var instructionsViewSize by remember { mutableStateOf(DpSize.Zero) } Row(modifier) { Column(modifier = Modifier.fillMaxHeight().fillMaxWidth(0.5f)) { uiState.visualInstruction?.let { instructions -> InstructionsView( instructions, + modifier = + Modifier.onSizeChanged { + instructionsViewSize = density.run { DpSize(it.width.toDp(), it.height.toDp()) } + }, remainingSteps = uiState.remainingSteps, distanceToNextManeuver = uiState.progress?.distanceToNextManeuver) } @@ -62,7 +75,13 @@ fun LandscapeNavigationOverlayView( Spacer(modifier = Modifier.weight(1f)) uiState.progress?.let { progress -> - TripProgressView(progress = progress, onTapExit = onTapExit) + TripProgressView( + modifier = + Modifier.onSizeChanged { + progressViewSize.value = density.run { DpSize(it.width.toDp(), it.height.toDp()) } + }, + progress = progress, + onTapExit = onTapExit) } } @@ -74,11 +93,17 @@ fun LandscapeNavigationOverlayView( showMute = config.showMute, isMuted = uiState.isMuted, onClickMute = { viewModel.toggleMute() }, + buttonSize = config.buttonSize, + cameraControlState = + config.cameraControlState( + camera, + navigationCamera, + uiState, + NavigationViewMetrics(progressViewSize.value, instructionsViewSize), + ), showZoom = config.showZoom, onClickZoomIn = { camera.value = camera.value.incrementZoom(1.0) }, - onClickZoomOut = { camera.value = camera.value.incrementZoom(-1.0) }, - showCentering = !cameraIsTrackingLocation, - onClickCenter = { camera.value = navigationCamera }) + onClickZoomOut = { camera.value = camera.value.incrementZoom(-1.0) }) } } } @@ -95,6 +120,7 @@ fun LandscapeNavigationOverlayViewPreview() { LandscapeNavigationOverlayView( modifier = Modifier.fillMaxSize(), camera = rememberSaveableMapViewCamera(), + navigationCamera = navigationMapViewCamera(), viewModel = viewModel, onTapExit = {}) } diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt index c2bf2fd8..596238a4 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged @@ -22,6 +23,7 @@ import com.maplibre.compose.camera.CameraState import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.camera.extensions.incrementZoom import com.maplibre.compose.rememberSaveableMapViewCamera +import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.composeui.views.CurrentRoadNameView import com.stadiamaps.ferrostar.composeui.views.InstructionsView import com.stadiamaps.ferrostar.composeui.views.TripProgressView @@ -30,7 +32,8 @@ 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.config.VisualNavigationViewConfig +import com.stadiamaps.ferrostar.maplibreui.NavigationViewMetrics +import com.stadiamaps.ferrostar.maplibreui.extensions.cameraControlState import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -39,7 +42,7 @@ import kotlinx.coroutines.flow.asStateFlow fun PortraitNavigationOverlayView( modifier: Modifier, camera: MutableState, - navigationCamera: MapViewCamera = navigationMapViewCamera(), + navigationCamera: MapViewCamera, viewModel: NavigationViewModel, config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(), progressViewSize: MutableState = remember { mutableStateOf(DpSize.Zero) }, @@ -53,11 +56,16 @@ fun PortraitNavigationOverlayView( ) { val density = LocalDensity.current val uiState by viewModel.uiState.collectAsState() + var instructionsViewSize by remember { mutableStateOf(DpSize.Zero) } Column(modifier) { uiState.visualInstruction?.let { instructions -> InstructionsView( instructions, + modifier = + Modifier.onSizeChanged { + instructionsViewSize = density.run { DpSize(it.width.toDp(), it.height.toDp()) } + }, remainingSteps = uiState.remainingSteps, distanceToNextManeuver = uiState.progress?.distanceToNextManeuver) } @@ -69,11 +77,17 @@ fun PortraitNavigationOverlayView( showMute = config.showMute, isMuted = uiState.isMuted, onClickMute = { viewModel.toggleMute() }, + buttonSize = config.buttonSize, + cameraControlState = + config.cameraControlState( + camera, + navigationCamera, + uiState, + NavigationViewMetrics(progressViewSize.value, instructionsViewSize), + ), showZoom = config.showZoom, onClickZoomIn = { camera.value = camera.value.incrementZoom(1.0) }, onClickZoomOut = { camera.value = camera.value.incrementZoom(-1.0) }, - showCentering = !cameraIsTrackingLocation, - onClickCenter = { camera.value = navigationCamera }, ) uiState.progress?.let { progress -> @@ -82,6 +96,7 @@ fun PortraitNavigationOverlayView( if (cameraIsTrackingLocation) { uiState.currentStepRoadName } else { + // Hide the road name view if not tracking the user location null } currentRoadName?.let { roadName -> currentRoadNameView(roadName) } @@ -106,6 +121,7 @@ fun PortraitNavigationOverlayViewPreview() { PortraitNavigationOverlayView( modifier = Modifier.fillMaxSize(), camera = rememberSaveableMapViewCamera(), + navigationCamera = navigationMapViewCamera(), viewModel = viewModel, onTapExit = {}) } diff --git a/android/maplibreui/src/test/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfigTest.kt b/android/maplibreui/src/test/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfigTest.kt index 2e1d5f67..6d5c247d 100644 --- a/android/maplibreui/src/test/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfigTest.kt +++ b/android/maplibreui/src/test/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfigTest.kt @@ -1,5 +1,12 @@ package com.stadiamaps.ferrostar.maplibreui.config +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig +import com.stadiamaps.ferrostar.composeui.config.buttonSize +import com.stadiamaps.ferrostar.composeui.config.useMuteButton +import com.stadiamaps.ferrostar.composeui.config.useZoomButton +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Test @@ -37,4 +44,11 @@ class VisualNavigationViewConfigTest { assert(config.showMute) assert(config.showZoom) } + + @Test + fun testButtonSize() { + val newSize = DpSize(42.dp, 42.dp) + val config = VisualNavigationViewConfig().buttonSize(newSize) + assertEquals(newSize, config.buttonSize) + } } diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/CurrentRoadViewTests/testDefaultCurrentRoadView.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/CurrentRoadViewTests/testDefaultCurrentRoadView.1.png index cb80ba99..23273bfd 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/CurrentRoadViewTests/testDefaultCurrentRoadView.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/CurrentRoadViewTests/testDefaultCurrentRoadView.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/CurrentRoadViewTests/testDefaultCurrentRoadViewFunkyStyle.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/CurrentRoadViewTests/testDefaultCurrentRoadViewFunkyStyle.1.png index 056091fb..e4f85d23 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/CurrentRoadViewTests/testDefaultCurrentRoadViewFunkyStyle.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/CurrentRoadViewTests/testDefaultCurrentRoadViewFunkyStyle.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/DefaultManeuverInstructionViewTests/testDefaultManeuverInstructionViewDE.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/DefaultManeuverInstructionViewTests/testDefaultManeuverInstructionViewDE.1.png index 2dccfad1..1ce511ee 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/DefaultManeuverInstructionViewTests/testDefaultManeuverInstructionViewDE.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/DefaultManeuverInstructionViewTests/testDefaultManeuverInstructionViewDE.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/DefaultManeuverInstructionViewTests/testDefaultManeuverInstructionViewUS.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/DefaultManeuverInstructionViewTests/testDefaultManeuverInstructionViewUS.1.png index b4caa1d5..9154bb24 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/DefaultManeuverInstructionViewTests/testDefaultManeuverInstructionViewUS.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/DefaultManeuverInstructionViewTests/testDefaultManeuverInstructionViewUS.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testExpandedInstructionsView.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testExpandedInstructionsView.1.png index 6ade82fa..0631457e 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testExpandedInstructionsView.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testExpandedInstructionsView.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testExpandedInstructionsView_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testExpandedInstructionsView_darkMode.1.png index 889cda8e..d5fcf108 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testExpandedInstructionsView_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testExpandedInstructionsView_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testFormattingDE.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testFormattingDE.1.png index e3dee473..3c25ddfe 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testFormattingDE.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testFormattingDE.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testInstructionsView.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testInstructionsView.1.png index a85668d0..0b2798b3 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testInstructionsView.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testInstructionsView.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testInstructionsView_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testInstructionsView_darkMode.1.png index 3bb92c52..a9b4a78f 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testInstructionsView_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testInstructionsView_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsView.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsView.1.png index 50e5f02f..bb336de5 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsView.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsView.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsViewWithPill.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsViewWithPill.1.png index 9c05bb0b..f0428b3f 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsViewWithPill.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsViewWithPill.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsViewWithPill_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsViewWithPill_darkMode.1.png index 76e85c51..b538dd15 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsViewWithPill_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/InstructionsViewTests/testSingularInstructionsViewWithPill_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testCustomManeuverInstructionIcon.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testCustomManeuverInstructionIcon.1.png index 0806d156..0dbca39c 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testCustomManeuverInstructionIcon.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testCustomManeuverInstructionIcon.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testFerrostarInstruction.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testFerrostarInstruction.1.png index caa262a6..f189599c 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testFerrostarInstruction.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testFerrostarInstruction.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testFerrostarInstructionDE.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testFerrostarInstructionDE.1.png index ed38a236..bca80122 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testFerrostarInstructionDE.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testFerrostarInstructionDE.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testRightToLeftInstruction.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testRightToLeftInstruction.1.png index d8e8c85c..5593991f 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testRightToLeftInstruction.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverInstructionViewTests/testRightToLeftInstruction.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_muted.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_muted.1.png index e37fa447..5ff54654 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_muted.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_muted.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_muted_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_muted_darkMode.1.png index b79f6187..c0bee653 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_muted_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_muted_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_unmuted.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_unmuted.1.png index f3d4d9c1..60560a24 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_unmuted.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_unmuted.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_unmuted_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_unmuted_darkMode.1.png index ebceb94d..1e82d2d6 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_unmuted_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/MuteButtonTests/test_unmuted_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_USStyle_speedLimit_inGridView.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_USStyle_speedLimit_inGridView.1.png index edee9464..7c53c4d8 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_USStyle_speedLimit_inGridView.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_USStyle_speedLimit_inGridView.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_ViennaConventionStyle_speedLimit_inGridView.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_ViennaConventionStyle_speedLimit_inGridView.1.png index 57e73e5c..03ab4071 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_ViennaConventionStyle_speedLimit_inGridView.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_ViennaConventionStyle_speedLimit_inGridView.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_muteIsHidden.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_muteIsHidden.1.png index 3aef3cea..4ad910cf 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_muteIsHidden.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigatingInnerGridViewTests/test_muteIsHidden.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testErrorBanner.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testErrorBanner.1.png index f30573f9..b09b254e 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testErrorBanner.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testErrorBanner.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testErrorBanner_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testErrorBanner_darkMode.1.png index f60c3f6f..0910dd85 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testErrorBanner_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testErrorBanner_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testInfoBanner.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testInfoBanner.1.png index 990b42ca..e2a2c638 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testInfoBanner.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testInfoBanner.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testInfoBanner_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testInfoBanner_darkMode.1.png index afa54567..2be77b9f 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testInfoBanner_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testInfoBanner_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testLoadingBanner.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testLoadingBanner.1.png index 0d999863..05dca644 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testLoadingBanner.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testLoadingBanner.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testLoadingBanner_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testLoadingBanner_darkMode.1.png index 90f935a2..d3560a2a 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testLoadingBanner_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIBannerViewTests/testLoadingBanner_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIButtonTests/testTextButton.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIButtonTests/testTextButton.1.png index 4c6077e5..f0209b9c 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIButtonTests/testTextButton.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIButtonTests/testTextButton.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIButtonTests/testTextButton_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIButtonTests/testTextButton_darkMode.1.png index c0ee0d71..e9afe499 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIButtonTests/testTextButton_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIButtonTests/testTextButton_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIZoomButtonTests/testNavigationUIZoomButton.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIZoomButtonTests/testNavigationUIZoomButton.1.png index c1a9c100..756c57cb 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIZoomButtonTests/testNavigationUIZoomButton.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIZoomButtonTests/testNavigationUIZoomButton.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIZoomButtonTests/testNavigationUIZoomButton_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIZoomButtonTests/testNavigationUIZoomButton_darkMode.1.png index ced1fd2b..5e27a8d0 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIZoomButtonTests/testNavigationUIZoomButton_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/NavigationUIZoomButtonTests/testNavigationUIZoomButton_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.1.png index 38d6a79b..68fc12f8 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.2.png index 0602b8a0..a8a3c82d 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.3.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.3.png index c485e9ae..a66ec7d8 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.3.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.3.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.4.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.4.png index 865c350a..671cfa8a 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.4.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews.4.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.1.png index 38d6a79b..68fc12f8 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.2.png index 0602b8a0..a8a3c82d 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.3.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.3.png index c485e9ae..a66ec7d8 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.3.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.3.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.4.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.4.png index 865c350a..671cfa8a 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.4.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testUSStyleSpeedLimitViews_darkMode.4.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.1.png index 80a33280..16e345e1 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.2.png index 241db65f..78954986 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.3.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.3.png index 73074b26..2da3abb6 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.3.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.3.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.4.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.4.png index 83f82b35..39eda591 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.4.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews.4.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.1.png index 80a33280..16e345e1 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.2.png index 241db65f..78954986 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.3.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.3.png index 73074b26..2da3abb6 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.3.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.3.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.4.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.4.png index 83f82b35..39eda591 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.4.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/SpeedLimitViewTests/testViennaConventionStyleSpeedLimitViews_darkMode.4.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewCompactTheme.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewCompactTheme.1.png index a4777ce8..c43a7998 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewCompactTheme.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewCompactTheme.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewCompactTheme_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewCompactTheme_darkMode.1.png index 43b3ad3f..199d7b23 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewCompactTheme_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewCompactTheme_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme.1.png index d6b73f01..283ba5ba 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme.2.png index ee64bc04..378d5592 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme_darkMode.1.png index 1e1f68bd..f87223a0 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme_darkMode.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme_darkMode.2.png index f0bd93fa..58360402 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme_darkMode.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewDefaultTheme_darkMode.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters.1.png index c90c9808..57264769 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_darkMode.1.png index c04de7be..340283a5 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE.1.png index 5792ab05..a8d98eff 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE.2.png index 6a467443..a0b29133 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE_darkMode.1.png index 79f874d7..0d59504e 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE_darkMode.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE_darkMode.2.png index 78c5daaf..c334da5e 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE_darkMode.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewTests/testTripProgressViewFormatters_de_DE_darkMode.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewCompactTheme.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewCompactTheme.1.png index 40828204..fb097661 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewCompactTheme.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewCompactTheme.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewCompactTheme_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewCompactTheme_darkMode.1.png index 18a8c889..64792650 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewCompactTheme_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewCompactTheme_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme.1.png index fe741353..6cf41a45 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme.2.png index 14d3ef1d..7f2addd8 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme_darkMode.1.png index c7fadac2..c7c28d94 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme_darkMode.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme_darkMode.2.png index ca29158f..8451a35c 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme_darkMode.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewDefaultTheme_darkMode.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters.1.png index 4f1867f3..3f7680c3 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_darkMode.1.png index 840d4bf4..426a41fb 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE.1.png index 895eefc9..2a133506 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE.2.png index 3a13b472..2af97a6e 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE.2.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE_darkMode.1.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE_darkMode.1.png index 932f27ef..63442838 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE_darkMode.1.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE_darkMode.1.png differ diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE_darkMode.2.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE_darkMode.2.png index 8eabd4e2..28fef2fa 100644 Binary files a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE_darkMode.2.png and b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/TripProgressViewWithButtonTests/testTripProgressViewFormatters_de_DE_darkMode.2.png differ diff --git a/common/Cargo.lock b/common/Cargo.lock index 16a54344..23bdbede 100644 --- a/common/Cargo.lock +++ b/common/Cargo.lock @@ -400,7 +400,7 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "ferrostar" -version = "0.20.1" +version = "0.21.0" dependencies = [ "assert-json-diff", "geo", diff --git a/common/ferrostar/Cargo.toml b/common/ferrostar/Cargo.toml index 5f650c8b..750571c0 100644 --- a/common/ferrostar/Cargo.toml +++ b/common/ferrostar/Cargo.toml @@ -2,7 +2,7 @@ lints.workspace = true [package] name = "ferrostar" -version = "0.20.1" +version = "0.21.0" readme = "README.md" description = "The core of modern turn-by-turn navigation." keywords = ["navigation", "routing", "valhalla", "osrm"] diff --git a/common/ferrostar/src/algorithms.rs b/common/ferrostar/src/algorithms.rs index 7f9f7b15..9f446d6f 100644 --- a/common/ferrostar/src/algorithms.rs +++ b/common/ferrostar/src/algorithms.rs @@ -353,7 +353,7 @@ fn distance_along(point: &Point, linestring: &LineString) -> Option { } let (_, _, traversed) = linestring.lines().try_fold( - (0f64, f64::INFINITY, 06f64), + (0f64, f64::INFINITY, 0f64), |(cum_length, closest_dist_to_point, traversed), segment| { // Convert to a LineString so we get haversine ops let segment_linestring = LineString::from(segment); diff --git a/guide/src/android-getting-started.md b/guide/src/android-getting-started.md index b53b5222..dc0e1a91 100644 --- a/guide/src/android-getting-started.md +++ b/guide/src/android-getting-started.md @@ -7,12 +7,14 @@ We'll cover the "batteries included" approach, but flag areas for customization ### Add dependencies +Let’s get started with Gradle setup. +Replace `X.Y.Z` with the latest [release version](https://central.sonatype.com/namespace/com.stadiamaps.ferrostar). + #### `build.gradle` with explicit version strings If you’re using the classic `build.gradle` with `implementation` strings using hard-coded versions, here’s how to set things up. -Replace `X.Y.Z` with the latest [release version](https://github.com/orgs/stadiamaps/packages?repo_name=ferrostar). ```groovy dependencies { @@ -108,34 +110,11 @@ and (if simulating a route) set the location manually or enter a simulated route Similar to the Android location APIs you may already know, you can add or remove listeners which will receive updates. -#### `AndroidSystemLocationProvider` - -The `AndroidSystemLocationProvider` uses the location provider -from the Android open-source project. -This is not as good as the proprietary Google fused location client, -but it is the most compatible option -as it will run even on “un-Googled” phones -and can be used in apps distributed on F-Droid. - -Initializing this provider requires an Android `Context`, -so you probably need to declare it as a `lateinit var` instance variable. - -```kotlin -private lateinit var locationProvider: AndroidSystemLocationProvider -``` - -You can initialize it like so. -In an `Activity`, the context is simply `this`. -In other cases, get a context using an appropriate method. - -```kotlin -locationProvider = AndroidSystemLocationProvider(context = this) -``` #### Google Play Fused Location Client -Alternatively, you can use the `FusedLocationProvider` -if your app uses Google Play Services. +If your app uses Google Play Services, +you can use the `FusedLocationProvider` This normally offers better device positioning than the default Android location provider on supported devices. To make use of it, @@ -154,6 +133,31 @@ private lateinit var locationProvider: FusedLocationProvider locationProvider = FusedLocationProvider(context = this) ``` +#### `AndroidSystemLocationProvider` + +The `AndroidSystemLocationProvider` uses the location provider +from the Android open-source project. +This is not as good as the proprietary Google fused location client, +but it will run on *any* Android phone, +including ones without Google Play Services. +It is also compatible with stores like F-Droid, +which require all apps use open-source software. + +Initializing this provider requires an Android `Context`, +so you probably need to declare it as a `lateinit var` instance variable. + +```kotlin +private lateinit var locationProvider: AndroidSystemLocationProvider +``` + +You can initialize it like so. +In an `Activity`, the context is simply `this`. +In other cases, get a context using an appropriate method. + +```kotlin +locationProvider = AndroidSystemLocationProvider(context = this) +``` + #### `SimulatedLocationProvider` The `SimulatedLocationProvider` allows for simulating location within Ferrostar @@ -215,7 +219,13 @@ private val core = profile = "bicycle", httpClient = httpClient, locationProvider = locationProvider, - foregroundServiceManager = foregroundServiceManager + foregroundServiceManager = foregroundServiceManager, + navigationControllerConfig = + NavigationControllerConfig( + StepAdvanceMode.RelativeLineStringDistance( + minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U), + RouteDeviationTracking.StaticThreshold(15U, 50.0), + CourseFiltering.SNAP_TO_ROUTE), ) ``` @@ -283,15 +293,14 @@ var navigationViewModel by remember { mutableStateOf(null) And then use it to store the result of your `startNavigation` invocation: ```kotlin -navigationViewModel = - core.startNavigation( - route = route, - config = - NavigationControllerConfig( - StepAdvanceMode.RelativeLineStringDistance( - minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U), - RouteDeviationTracking.StaticThreshold(25U, 10.0)), - ) +core.startNavigation( + route = route, + config = + NavigationControllerConfig( + StepAdvanceMode.RelativeLineStringDistance( + minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U), + RouteDeviationTracking.StaticThreshold(25U, 10.0)), +) ``` Finally, If you’re simulating route progress @@ -304,30 +313,24 @@ locationProvider.setSimulatedRoute(route) ## Using the `DynamicallyOrientingNavigationView` -We’re finally ready to turn that view model into a beautiful navigation map! -It’s really as simple as creating a `DynamicallyOrientingNavigationView` with the view model. +We’re finally ready to put this together into a beautiful navigation map! +`FerrostarCore` exposes a state flow, +which you can incorporate into your own view model, +which must implement the `NavigationViewModel` interface. +See the `DemoNavigationViewModel` for an example of what a view model might look like. + Here’s an example: ```kotlin - val viewModel = navigationViewModel - if (viewModel != null) { - // You can get a free Stadia Maps API key at https://client.stadiamaps.com. - // See https://stadiamaps.github.io/ferrostar/vendors.html for additional vendors - DynamicallyOrientingNavigationView( - styleUrl = - "https://tiles.stadiamaps.com/styles/outdoors.json?api_key=$stadiaApiKey", - viewModel = viewModel) { uiState -> - // You can add your own overlays here! - // See https://github.com/Rallista/maplibre-compose-playground - } - } else { - // Loading indicator - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = "Calculating route...") - CircularProgressIndicator(modifier = Modifier.width(64.dp)) - } + // You can get a free Stadia Maps API key at https://client.stadiamaps.com. + // See https://stadiamaps.github.io/ferrostar/vendors.html for additional vendors + DynamicallyOrientingNavigationView( + styleUrl = + "https://tiles.stadiamaps.com/styles/outdoors.json?api_key=$stadiaApiKey", + viewModel = viewModel) { uiState -> + // You can add your own overlays here! + // See the DemoNavigationScene or https://github.com/Rallista/maplibre-compose-playground + // for some examples. } ``` @@ -337,7 +340,8 @@ Here’s an example: ## Demo app -We've put together a minimal [demo app](https://github.com/stadiamaps/ferrostar/tree/main/android/demo-app) with an example integration. +We've put together a minimal [demo app](https://github.com/stadiamaps/ferrostar/tree/main/android/demo-app) +to show how to integrate Ferrostar into your Android app. ## Going deeper diff --git a/web/package-lock.json b/web/package-lock.json index 31670a54..16efde1b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "@stadiamaps/ferrostar-webcomponents", - "version": "0.20.1", + "version": "0.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@stadiamaps/ferrostar-webcomponents", - "version": "0.20.1", + "version": "0.21.0", "license": "BSD-3-Clause", "dependencies": { "@stadiamaps/ferrostar": "file:../common/ferrostar/pkg", diff --git a/web/package.json b/web/package.json index dd695c87..cb40352a 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,7 @@ "CatMe0w (https://github.com/CatMe0w)", "Luke Seelenbinder " ], - "version": "0.20.1", + "version": "0.21.0", "license": "BSD-3-Clause", "type": "module", "main": "./dist/ferrostar-webcomponents.js",