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 index f2579a86..c9458834 100644 --- 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 @@ -1,11 +1,16 @@ package com.stadiamaps.ferrostar.composeui.config +import com.stadiamaps.ferrostar.composeui.views.components.speedlimit.SignageStyle + data class VisualNavigationViewConfig( // Mute var showMute: Boolean = false, // Zoom var showZoom: Boolean = false, + + // Speed Limit + var speedLimitStyle: SignageStyle? = null, ) { companion object { fun Default() = VisualNavigationViewConfig(showMute = true, showZoom = true) @@ -21,3 +26,9 @@ fun VisualNavigationViewConfig.useMuteButton(): VisualNavigationViewConfig { fun VisualNavigationViewConfig.useZoomButton(): VisualNavigationViewConfig { return copy(showZoom = true) } + +fun VisualNavigationViewConfig.withSpeedLimitStyle( + style: SignageStyle +): VisualNavigationViewConfig { + return copy(speedLimitStyle = style) +} diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/formatting/MeasurementSpeedFormatter.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/formatting/MeasurementSpeedFormatter.kt index 2fc1c786..1bff4647 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/formatting/MeasurementSpeedFormatter.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/formatting/MeasurementSpeedFormatter.kt @@ -1,12 +1,24 @@ package com.stadiamaps.ferrostar.composeui.formatting +import android.content.Context import android.icu.util.ULocale import com.stadiamaps.ferrostar.composeui.measurement.localizedString import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeed -import com.stadiamaps.ferrostar.core.measurement.SpeedUnit +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeedUnit import java.util.Locale -class MeasurementSpeedFormatter(val measurementSpeed: MeasurementSpeed) { +class MeasurementSpeedFormatter(context: Context, val measurementSpeed: MeasurementSpeed) { + + // This allows us to avoid capturing the context downstream + private val unitLocalizations = + mapOf( + MeasurementSpeedUnit.MetersPerSecond to + MeasurementSpeedUnit.MetersPerSecond.localizedString(context), + MeasurementSpeedUnit.MilesPerHour to + MeasurementSpeedUnit.MilesPerHour.localizedString(context), + MeasurementSpeedUnit.KilometersPerHour to + MeasurementSpeedUnit.KilometersPerHour.localizedString(context), + MeasurementSpeedUnit.Knots to MeasurementSpeedUnit.Knots.localizedString(context)) fun formattedValue(locale: ULocale = ULocale.getDefault()): String { val locale = locale.let { Locale(it.language, it.country) } @@ -15,17 +27,17 @@ class MeasurementSpeedFormatter(val measurementSpeed: MeasurementSpeed) { fun formattedValue( locale: ULocale = ULocale.getDefault(), - converted: SpeedUnit = measurementSpeed.unit + converted: MeasurementSpeedUnit = measurementSpeed.unit ): String { val locale = locale.let { Locale(it.language, it.country) } return String.format(locale = locale, "%.0f", measurementSpeed.value(converted)) } fun formatted(): String { - return "${measurementSpeed.value} ${measurementSpeed.unit.localizedString()}" + return "${measurementSpeed.value} ${unitLocalizations[measurementSpeed.unit]}" } - fun formatted(converted: SpeedUnit): String { - return "${measurementSpeed.value(converted)} ${converted.localizedString()}" + fun formatted(converted: MeasurementSpeedUnit): String { + return "${measurementSpeed.value(converted)} ${unitLocalizations[converted]}" } } diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/measurement/LocalizedSpeed.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/measurement/LocalizedSpeed.kt index d2285479..2625337a 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/measurement/LocalizedSpeed.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/measurement/LocalizedSpeed.kt @@ -1,13 +1,14 @@ package com.stadiamaps.ferrostar.composeui.measurement -import android.content.res.Resources +import android.content.Context import com.stadiamaps.ferrostar.composeui.R -import com.stadiamaps.ferrostar.core.measurement.SpeedUnit +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeedUnit -fun SpeedUnit.localizedString(): String { +fun MeasurementSpeedUnit.localizedString(context: Context): String { return when (this) { - SpeedUnit.MetersPerSecond -> Resources.getSystem().getString(R.string.unit_short_mps) - SpeedUnit.MilesPerHour -> Resources.getSystem().getString(R.string.unit_short_kph) - SpeedUnit.KilometersPerHour -> Resources.getSystem().getString(R.string.unit_short_mph) + MeasurementSpeedUnit.MetersPerSecond -> context.getString(R.string.unit_short_mps) + MeasurementSpeedUnit.MilesPerHour -> context.getString(R.string.unit_short_mph) + MeasurementSpeedUnit.KilometersPerHour -> context.getString(R.string.unit_short_kph) + MeasurementSpeedUnit.Knots -> context.getString(R.string.unit_short_knot) } } diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/support/GreenScreenPreview.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/support/GreenScreenPreview.kt new file mode 100644 index 00000000..fa093efe --- /dev/null +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/support/GreenScreenPreview.kt @@ -0,0 +1,16 @@ +package com.stadiamaps.ferrostar.composeui.support + +import android.content.res.Configuration +import androidx.compose.ui.tooling.preview.Preview + +@Preview( + name = "Dark Mode", + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES, + backgroundColor = 0xFF93C97C) +@Preview( + name = "Light Mode", + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_NO, + backgroundColor = 0xFF93C97C) +internal annotation class GreenScreenPreview diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/gridviews/NavigatingInnerGridView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/gridviews/NavigatingInnerGridView.kt index 4554100e..2e2de9ba 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/gridviews/NavigatingInnerGridView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/gridviews/NavigatingInnerGridView.kt @@ -23,10 +23,16 @@ import com.stadiamaps.ferrostar.composeui.R import com.stadiamaps.ferrostar.composeui.models.CameraControlState import com.stadiamaps.ferrostar.composeui.views.components.controls.NavigationUIButton import com.stadiamaps.ferrostar.composeui.views.components.controls.NavigationUIZoomButton +import com.stadiamaps.ferrostar.composeui.views.components.speedlimit.SignageStyle +import com.stadiamaps.ferrostar.composeui.views.components.speedlimit.SpeedLimitView +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeed +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeedUnit @Composable fun NavigatingInnerGridView( modifier: Modifier, + speedLimit: MeasurementSpeed? = null, + speedLimitStyle: SignageStyle? = null, showMute: Boolean = true, isMuted: Boolean?, onClickMute: () -> Unit = {}, @@ -43,7 +49,9 @@ fun NavigatingInnerGridView( InnerGridView( modifier, topStart = { - // TODO: SpeedLimitView goes here + speedLimit?.let { + speedLimitStyle?.let { style -> SpeedLimitView(speedLimit = it, signageStyle = style) } + } }, topCenter = topCenter, topEnd = { @@ -108,6 +116,8 @@ fun NavigatingInnerGridView( fun NavigatingInnerGridViewNonTrackingPreview() { NavigatingInnerGridView( modifier = Modifier.fillMaxSize(), + speedLimit = MeasurementSpeed(24.6, MeasurementSpeedUnit.MetersPerSecond), + speedLimitStyle = SignageStyle.MUTCD, isMuted = false, buttonSize = DpSize(56.dp, 56.dp), cameraControlState = @@ -121,6 +131,8 @@ fun NavigatingInnerGridViewNonTrackingPreview() { fun NavigatingInnerGridViewTrackingPreview() { NavigatingInnerGridView( modifier = Modifier.fillMaxSize(), + speedLimit = MeasurementSpeed(24.6, MeasurementSpeedUnit.MetersPerSecond), + speedLimitStyle = SignageStyle.MUTCD, isMuted = false, buttonSize = DpSize(56.dp, 56.dp), cameraControlState = @@ -136,6 +148,8 @@ fun NavigatingInnerGridViewTrackingPreview() { fun NavigatingInnerGridViewLandscapeNonTrackingPreview() { NavigatingInnerGridView( modifier = Modifier.fillMaxSize(), + speedLimit = MeasurementSpeed(27.8, MeasurementSpeedUnit.MetersPerSecond), + speedLimitStyle = SignageStyle.ViennaConvention, isMuted = true, buttonSize = DpSize(56.dp, 56.dp), cameraControlState = @@ -151,6 +165,8 @@ fun NavigatingInnerGridViewLandscapeNonTrackingPreview() { fun NavigatingInnerGridViewLandscapeTrackingPreview() { NavigatingInnerGridView( modifier = Modifier.fillMaxSize(), + speedLimit = MeasurementSpeed(27.8, MeasurementSpeedUnit.MetersPerSecond), + speedLimitStyle = SignageStyle.ViennaConvention, isMuted = true, buttonSize = DpSize(56.dp, 56.dp), cameraControlState = diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/speedlimit/SpeedLimitView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/speedlimit/SpeedLimitView.kt new file mode 100644 index 00000000..efa025cf --- /dev/null +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/speedlimit/SpeedLimitView.kt @@ -0,0 +1,39 @@ +package com.stadiamaps.ferrostar.composeui.views.components.speedlimit + +import android.content.Context +import android.icu.util.ULocale +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.stadiamaps.ferrostar.composeui.formatting.MeasurementSpeedFormatter +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeed +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeedUnit + +enum class SignageStyle { + MUTCD, + ViennaConvention +} + +@Composable +fun SpeedLimitView( + modifier: Modifier = Modifier, + speedLimit: MeasurementSpeed, + signageStyle: SignageStyle, + context: Context = LocalContext.current, + formatter: MeasurementSpeedFormatter = MeasurementSpeedFormatter(context, speedLimit), + locale: ULocale = ULocale.getDefault() +) { + when (signageStyle) { + SignageStyle.MUTCD -> + USStyleSpeedLimitView( + modifier, speedLimit, MeasurementSpeedUnit.MilesPerHour, context, formatter, locale) + SignageStyle.ViennaConvention -> + ViennaConventionStyleSpeedLimitView( + modifier, + speedLimit, + MeasurementSpeedUnit.KilometersPerHour, + context, + formatter, + locale) + } +} diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/speedlimit/USStyleSpeedLimitView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/speedlimit/USStyleSpeedLimitView.kt new file mode 100644 index 00000000..cc2f33b5 --- /dev/null +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/speedlimit/USStyleSpeedLimitView.kt @@ -0,0 +1,126 @@ +package com.stadiamaps.ferrostar.composeui.views.components.speedlimit + +import android.content.Context +import android.icu.util.ULocale +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.stadiamaps.ferrostar.composeui.R +import com.stadiamaps.ferrostar.composeui.formatting.MeasurementSpeedFormatter +import com.stadiamaps.ferrostar.composeui.measurement.localizedString +import com.stadiamaps.ferrostar.composeui.support.GreenScreenPreview +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeed +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeedUnit + +@Composable +fun USStyleSpeedLimitView( + modifier: Modifier = Modifier, + speedLimit: MeasurementSpeed, + units: MeasurementSpeedUnit = MeasurementSpeedUnit.MilesPerHour, + context: Context = LocalContext.current, + formatter: MeasurementSpeedFormatter = MeasurementSpeedFormatter(context, speedLimit), + locale: ULocale = ULocale.getDefault() +) { + val formattedSpeed = formatter.formattedValue(locale, units) + + Box( + modifier = + modifier + .height(84.dp) + .width(60.dp) + .background(color = Color.White, shape = RoundedCornerShape(8.dp)) + .padding(2.dp)) { + Box( + modifier = + Modifier.height(80.dp) + .width(56.dp) + .background(color = Color.Black, shape = RoundedCornerShape(6.dp)) + .padding(2.dp)) { + Box( + modifier = + Modifier.height(76.dp) + .width(52.dp) + .background(color = Color.White, shape = RoundedCornerShape(4.dp)) + .padding(4.dp)) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center) { + Text( + text = stringResource(R.string.speed).uppercase(), + fontSize = 9.sp, + lineHeight = 10.sp, + fontWeight = FontWeight.Bold, + color = Color.Black) + + Text( + text = stringResource(R.string.limit).uppercase(), + fontSize = 9.sp, + lineHeight = 10.sp, + fontWeight = FontWeight.Bold, + color = Color.Black) + + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = formattedSpeed, + fontSize = if (formattedSpeed.length > 3) 18.sp else 24.sp, + lineHeight = if (formattedSpeed.length > 3) 20.sp else 26.sp, + fontWeight = FontWeight.ExtraBold, + color = Color.Black, + textAlign = TextAlign.Center) + + Text( + text = units.localizedString(context), + fontSize = 9.sp, + lineHeight = 10.sp, + fontWeight = FontWeight.Bold, + color = Color.Gray) + } + } + } + } +} + +@GreenScreenPreview +@Composable +fun USStyleSpeedLimitViewLowSpeedPreview() { + USStyleSpeedLimitView( + modifier = Modifier.padding(16.dp).shadow(4.dp), + speedLimit = MeasurementSpeed(55.0, MeasurementSpeedUnit.MilesPerHour)) +} + +@GreenScreenPreview +@Composable +fun USStyleSpeedLimitViewModerateSpeedPreview() { + USStyleSpeedLimitView( + modifier = Modifier.padding(16.dp).shadow(4.dp), + speedLimit = MeasurementSpeed(100.0, MeasurementSpeedUnit.MilesPerHour)) +} + +@GreenScreenPreview +@Composable +fun USStyleSpeedLimitViewHighSpeedPreview() { + USStyleSpeedLimitView( + modifier = Modifier.padding(16.dp).shadow(4.dp), + speedLimit = MeasurementSpeed(1000.0, MeasurementSpeedUnit.MilesPerHour)) +} diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/speedlimit/ViennaConventionStyleSpeedLimitView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/speedlimit/ViennaConventionStyleSpeedLimitView.kt new file mode 100644 index 00000000..0abadd1c --- /dev/null +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/speedlimit/ViennaConventionStyleSpeedLimitView.kt @@ -0,0 +1,113 @@ +package com.stadiamaps.ferrostar.composeui.views.components.speedlimit + +import android.content.Context +import android.icu.util.ULocale +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.stadiamaps.ferrostar.composeui.formatting.MeasurementSpeedFormatter +import com.stadiamaps.ferrostar.composeui.measurement.localizedString +import com.stadiamaps.ferrostar.composeui.support.GreenScreenPreview +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeed +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeedUnit + +@Composable +fun ViennaConventionStyleSpeedLimitView( + modifier: Modifier = Modifier, + speedLimit: MeasurementSpeed, + units: MeasurementSpeedUnit = MeasurementSpeedUnit.KilometersPerHour, + context: Context = LocalContext.current, + formatter: MeasurementSpeedFormatter = MeasurementSpeedFormatter(context, speedLimit), + locale: ULocale = ULocale.getDefault() +) { + val formattedSpeed = formatter.formattedValue(locale, units) + + Box( + modifier = + modifier + .height(64.dp) + .width(64.dp) + .background(color = Color.Red, shape = RoundedCornerShape(50)) + .padding(6.dp)) { + Box( + modifier = + Modifier.height(56.dp) + .width(56.dp) + .background(color = Color.White, shape = RoundedCornerShape(50)) + .padding(4.dp)) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center) { + Text( + text = formattedSpeed, + fontSize = + when { + formattedSpeed.length > 3 -> 14.sp + formattedSpeed.length > 2 -> 18.sp + else -> 24.sp + }, + fontWeight = FontWeight.ExtraBold, + lineHeight = + when { + formattedSpeed.length > 3 -> 16.sp + formattedSpeed.length > 2 -> 20.sp + else -> 26.sp + }, + color = Color.Black, + textAlign = TextAlign.Center) + + Text( + text = units.localizedString(context), + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + lineHeight = 10.sp, + color = Color.Gray) + } + } + } +} + +@GreenScreenPreview +@Composable +fun ViennaConventionStyleSpeedLimitViewLowSpeedPreview() { + ViennaConventionStyleSpeedLimitView( + modifier = Modifier.padding(16.dp).shadow(4.dp, RoundedCornerShape(50)), + speedLimit = MeasurementSpeed(30.0, MeasurementSpeedUnit.KilometersPerHour), + units = MeasurementSpeedUnit.KilometersPerHour) +} + +@GreenScreenPreview +@Composable +fun ViennaConventionStyleSpeedLimitViewModerateSpeedPreview() { + ViennaConventionStyleSpeedLimitView( + modifier = Modifier.padding(16.dp).shadow(4.dp, RoundedCornerShape(50)), + speedLimit = MeasurementSpeed(300.0, MeasurementSpeedUnit.KilometersPerHour), + units = MeasurementSpeedUnit.KilometersPerHour) +} + +@GreenScreenPreview +@Composable +fun ViennaConventionStyleSpeedLimitViewHighSpeedPreview() { + ViennaConventionStyleSpeedLimitView( + modifier = Modifier.padding(16.dp).shadow(4.dp, RoundedCornerShape(50)), + speedLimit = MeasurementSpeed(1000.0, MeasurementSpeedUnit.KilometersPerHour), + units = MeasurementSpeedUnit.KilometersPerHour) +} diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/LandscapeNavigationOverlayView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/LandscapeNavigationOverlayView.kt index 828643ee..8045964f 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/LandscapeNavigationOverlayView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/LandscapeNavigationOverlayView.kt @@ -92,6 +92,8 @@ fun LandscapeNavigationOverlayView( Column(modifier = Modifier.fillMaxHeight()) { NavigatingInnerGridView( modifier = Modifier.fillMaxSize(), + speedLimit = uiState.currentAnnotation?.speedLimit, + speedLimitStyle = config.speedLimitStyle, showMute = config.showMute, isMuted = uiState.isMuted, onClickMute = { viewModel.toggleMute() }, diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/PortraitNavigationOverlayView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/PortraitNavigationOverlayView.kt index 1605417c..b1943dbe 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/PortraitNavigationOverlayView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/PortraitNavigationOverlayView.kt @@ -73,6 +73,8 @@ fun PortraitNavigationOverlayView( NavigatingInnerGridView( modifier = Modifier.fillMaxSize().weight(1f).padding(bottom = 16.dp, top = 16.dp), + speedLimit = uiState.currentAnnotation?.speedLimit, + speedLimitStyle = config.speedLimitStyle, showMute = config.showMute, isMuted = uiState.isMuted, onClickMute = { viewModel.toggleMute() }, diff --git a/android/composeui/src/main/res/values/strings.xml b/android/composeui/src/main/res/values/strings.xml index 4d2065c8..fd396169 100644 --- a/android/composeui/src/main/res/values/strings.xml +++ b/android/composeui/src/main/res/values/strings.xml @@ -14,12 +14,16 @@ Maneuver instruction image • Instruction image - Preparing... + Preparing… Arrived You have arrived at your destination. Route Overview + Speed + Limit + m/s km/h mph + kn \ No newline at end of file diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/USStyleSpeedLimitViewTest.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/USStyleSpeedLimitViewTest.kt new file mode 100644 index 00000000..e96d399d --- /dev/null +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/USStyleSpeedLimitViewTest.kt @@ -0,0 +1,65 @@ +package com.stadiamaps.ferrostar.views + +import com.stadiamaps.ferrostar.composeui.views.components.speedlimit.USStyleSpeedLimitView +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeed +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeedUnit +import com.stadiamaps.ferrostar.support.paparazziDefault +import com.stadiamaps.ferrostar.support.withSnapshotBackground +import org.junit.Rule +import org.junit.Test + +class USStyleSpeedLimitViewTest { + @get:Rule val paparazzi = paparazziDefault() + + @Test + fun testLowSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + USStyleSpeedLimitView( + speedLimit = MeasurementSpeed(55.0, MeasurementSpeedUnit.MilesPerHour)) + } + } + } + + @Test + fun testFastSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + USStyleSpeedLimitView( + speedLimit = MeasurementSpeed(100.0, MeasurementSpeedUnit.MilesPerHour)) + } + } + } + + @Test + fun testImplausibleSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + USStyleSpeedLimitView( + speedLimit = MeasurementSpeed(1000.0, MeasurementSpeedUnit.MilesPerHour)) + } + } + } + + @Test + fun testKilometersPerHourSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + USStyleSpeedLimitView( + speedLimit = MeasurementSpeed(100.0, MeasurementSpeedUnit.KilometersPerHour), + units = MeasurementSpeedUnit.KilometersPerHour) + } + } + } + + @Test + fun testKnotsSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + USStyleSpeedLimitView( + speedLimit = MeasurementSpeed(100.0, MeasurementSpeedUnit.Knots), + units = MeasurementSpeedUnit.Knots) + } + } + } +} diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/ViennaStyleSpeedLimitViewTest.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/ViennaStyleSpeedLimitViewTest.kt new file mode 100644 index 00000000..e2204f95 --- /dev/null +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/ViennaStyleSpeedLimitViewTest.kt @@ -0,0 +1,76 @@ +package com.stadiamaps.ferrostar.views + +import com.stadiamaps.ferrostar.composeui.views.components.speedlimit.ViennaConventionStyleSpeedLimitView +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeed +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeedUnit +import com.stadiamaps.ferrostar.support.paparazziDefault +import com.stadiamaps.ferrostar.support.withSnapshotBackground +import org.junit.Rule +import org.junit.Test + +class ViennaStyleSpeedLimitViewTest { + @get:Rule val paparazzi = paparazziDefault() + + @Test + fun testLowSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + ViennaConventionStyleSpeedLimitView( + speedLimit = MeasurementSpeed(55.0, MeasurementSpeedUnit.KilometersPerHour)) + } + } + } + + @Test + fun testFastSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + ViennaConventionStyleSpeedLimitView( + speedLimit = MeasurementSpeed(100.0, MeasurementSpeedUnit.KilometersPerHour)) + } + } + } + + @Test + fun testImplausibleSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + ViennaConventionStyleSpeedLimitView( + speedLimit = MeasurementSpeed(1000.0, MeasurementSpeedUnit.KilometersPerHour)) + } + } + } + + @Test + fun testMetersPerSecondSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + ViennaConventionStyleSpeedLimitView( + speedLimit = MeasurementSpeed(100.0, MeasurementSpeedUnit.MetersPerSecond), + units = MeasurementSpeedUnit.MetersPerSecond) + } + } + } + + @Test + fun testMilesPerHourSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + ViennaConventionStyleSpeedLimitView( + speedLimit = MeasurementSpeed(100.0, MeasurementSpeedUnit.MilesPerHour), + units = MeasurementSpeedUnit.MilesPerHour) + } + } + } + + @Test + fun testKnotsSpeedValue() { + paparazzi.snapshot { + withSnapshotBackground { + ViennaConventionStyleSpeedLimitView( + speedLimit = MeasurementSpeed(100.0, MeasurementSpeedUnit.Knots), + units = MeasurementSpeedUnit.Knots) + } + } + } +} 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 index 6a3212e4..bef62a42 100644 Binary files a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewNonTracking.png 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 index 6a3212e4..0fa401eb 100644 Binary files a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewNonTrackingLandscape.png 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 index 82e5a057..a5c6b480 100644 Binary files a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewTracking.png 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 index db8afdc0..49413483 100644 Binary files a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewTrackingLandscape.png and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_NavigatingInnerGridViewTest_testNavigatingInnerGridViewTrackingLandscape.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testFastSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testFastSpeedValue.png new file mode 100644 index 00000000..3b24ab9d Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testFastSpeedValue.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testImplausibleSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testImplausibleSpeedValue.png new file mode 100644 index 00000000..838f9eb4 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testImplausibleSpeedValue.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testKilometersPerHourSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testKilometersPerHourSpeedValue.png new file mode 100644 index 00000000..bcaf7ce3 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testKilometersPerHourSpeedValue.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testKnotsSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testKnotsSpeedValue.png new file mode 100644 index 00000000..b95d5589 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testKnotsSpeedValue.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testLowSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testLowSpeedValue.png new file mode 100644 index 00000000..971aaad2 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_USStyleSpeedLimitViewTest_testLowSpeedValue.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testFastSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testFastSpeedValue.png new file mode 100644 index 00000000..8dbe1938 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testFastSpeedValue.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testImplausibleSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testImplausibleSpeedValue.png new file mode 100644 index 00000000..b1b94dc0 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testImplausibleSpeedValue.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testKnotsSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testKnotsSpeedValue.png new file mode 100644 index 00000000..1773e7db Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testKnotsSpeedValue.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testLowSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testLowSpeedValue.png new file mode 100644 index 00000000..fa882211 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testLowSpeedValue.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testMetersPerSecondSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testMetersPerSecondSpeedValue.png new file mode 100644 index 00000000..474f19ee Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testMetersPerSecondSpeedValue.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testMilesPerHourSpeedValue.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testMilesPerHourSpeedValue.png new file mode 100644 index 00000000..ef0de937 Binary files /dev/null and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_ViennaStyleSpeedLimitViewTest_testMilesPerHourSpeedValue.png differ 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 d6d58a79..53ff7288 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 @@ -4,6 +4,7 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.stadiamaps.ferrostar.core.annotation.AnnotationPublisher +import com.stadiamaps.ferrostar.core.annotation.AnnotationWrapper import com.stadiamaps.ferrostar.core.annotation.NoOpAnnotationPublisher import com.stadiamaps.ferrostar.core.extensions.currentRoadName import com.stadiamaps.ferrostar.core.extensions.deviation @@ -57,14 +58,17 @@ data class NavigationUiState( /** The name of the road which the current route step is traversing. */ val currentStepRoadName: String?, /** The remaining steps in the trip (including the current step). */ - val remainingSteps: List? + val remainingSteps: List?, + /** The route annotation object at the current location. */ + val currentAnnotation: AnnotationWrapper<*>? ) { companion object { fun fromFerrostar( coreState: NavigationState, isMuted: Boolean?, location: UserLocation?, - snappedLocation: UserLocation? + snappedLocation: UserLocation?, + annotation: AnnotationWrapper<*>? = null ): NavigationUiState = NavigationUiState( snappedLocation = snappedLocation, @@ -79,7 +83,8 @@ data class NavigationUiState( routeDeviation = coreState.tripState.deviation(), isMuted = isMuted, currentStepRoadName = coreState.tripState.currentRoadName(), - remainingSteps = coreState.tripState.remainingSteps()) + remainingSteps = coreState.tripState.remainingSteps(), + currentAnnotation = annotation) } fun isNavigating(): Boolean = progress != null @@ -114,9 +119,11 @@ open class DefaultNavigationViewModel( override val navigationUiState = combine(ferrostarCore.state, muteState) { a, b -> a to b } - .map { (coreState, muteState) -> annotationPublisher.map(coreState) to muteState } - .map { (stateWrapper, muteState) -> - val coreState = stateWrapper.state + .map { (coreState, muteState) -> + Triple(coreState, muteState, annotationPublisher.map(coreState)) + } + // The following converts coreState into an annotations wrapped state. + .map { (coreState, muteState, annotationWrapper) -> val location = ferrostarCore.locationProvider.lastLocation val userLocation = when (coreState.tripState) { @@ -124,7 +131,7 @@ open class DefaultNavigationViewModel( is TripState.Complete, TripState.Idle -> ferrostarCore.locationProvider.lastLocation } - uiState(coreState, muteState, location, userLocation) + uiState(coreState, muteState, location, userLocation, annotationWrapper) // This awkward dance is required because Kotlin doesn't have a way to map over // StateFlows // without converting to a generic Flow in the process. @@ -137,7 +144,8 @@ open class DefaultNavigationViewModel( ferrostarCore.state.value, ferrostarCore.spokenInstructionObserver?.isMuted, ferrostarCore.locationProvider.lastLocation, - ferrostarCore.locationProvider.lastLocation)) + ferrostarCore.locationProvider.lastLocation, + null)) override fun stopNavigation(stopLocationUpdates: Boolean) { ferrostarCore.stopNavigation(stopLocationUpdates = stopLocationUpdates) @@ -158,6 +166,9 @@ open class DefaultNavigationViewModel( coreState: NavigationState, isMuted: Boolean?, location: UserLocation?, - snappedLocation: UserLocation? - ) = NavigationUiState.fromFerrostar(coreState, isMuted, location, snappedLocation) + snappedLocation: UserLocation?, + annotationWrapper: AnnotationWrapper<*>? + ) = + NavigationUiState.fromFerrostar( + coreState, isMuted, location, snappedLocation, annotationWrapper) } diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/AnnotationWrapper.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/AnnotationWrapper.kt index 4170c900..cf461dac 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/AnnotationWrapper.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/AnnotationWrapper.kt @@ -1,9 +1,23 @@ package com.stadiamaps.ferrostar.core.annotation -import com.stadiamaps.ferrostar.core.NavigationState +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeed +import com.stadiamaps.ferrostar.core.measurement.MeasurementSpeedUnit -data class AnnotationWrapper( - val annotation: T? = null, - val speed: Speed? = null, - val state: NavigationState -) +data class AnnotationWrapper(val annotation: T? = null, val speed: Speed? = null) { + val speedLimit: MeasurementSpeed? + get() = + when (speed) { + is Speed.Value -> { + when (speed.unit) { + SpeedUnit.KILOMETERS_PER_HOUR -> + MeasurementSpeed(speed.value, MeasurementSpeedUnit.KilometersPerHour) + SpeedUnit.MILES_PER_HOUR -> + MeasurementSpeed(speed.value, MeasurementSpeedUnit.MilesPerHour) + SpeedUnit.KNOTS -> MeasurementSpeed(speed.value, MeasurementSpeedUnit.Knots) + } + } + else -> null + } + + companion object +} diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/DefaultAnnotationPublisher.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/DefaultAnnotationPublisher.kt index ca431dd9..0b03c19a 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/DefaultAnnotationPublisher.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/DefaultAnnotationPublisher.kt @@ -12,7 +12,7 @@ class DefaultAnnotationPublisher( override fun map(state: NavigationState): AnnotationWrapper { val annotations = decodeAnnotations(state) - return AnnotationWrapper(annotations, speedLimitMapper(annotations), state) + return AnnotationWrapper(annotations, speedLimitMapper(annotations)) } private fun decodeAnnotations(state: NavigationState): T? { diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/NoOpAnnotationPublisher.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/NoOpAnnotationPublisher.kt index 2f7c388f..49b51119 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/NoOpAnnotationPublisher.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/annotation/NoOpAnnotationPublisher.kt @@ -4,6 +4,6 @@ import com.stadiamaps.ferrostar.core.NavigationState class NoOpAnnotationPublisher : AnnotationPublisher { override fun map(state: NavigationState): AnnotationWrapper { - return AnnotationWrapper(state = state) + return AnnotationWrapper() } } diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/measurement/MeasurementSpeed.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/measurement/MeasurementSpeed.kt index 6cf1a850..94b7ab43 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/measurement/MeasurementSpeed.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/measurement/MeasurementSpeed.kt @@ -1,42 +1,59 @@ package com.stadiamaps.ferrostar.core.measurement -enum class SpeedUnit { +enum class MeasurementSpeedUnit { MetersPerSecond, MilesPerHour, - KilometersPerHour + KilometersPerHour, + Knots } -class MeasurementSpeed(val value: Double, val unit: SpeedUnit) { +class MeasurementSpeed(val value: Double, val unit: MeasurementSpeedUnit) { companion object { // TODO: Move this to a shared conversions constants file? const val METERS_PER_SECOND_TO_MILES_PER_HOUR = 2.23694 const val METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR = 3.6 + const val METERS_PER_SECOND_TO_KNOTS = 1.94384 } - fun value(converted: SpeedUnit): Double { + fun value(converted: MeasurementSpeedUnit): Double { when (unit) { - SpeedUnit.MetersPerSecond -> { + MeasurementSpeedUnit.MetersPerSecond -> { return when (converted) { - SpeedUnit.MetersPerSecond -> value - SpeedUnit.MilesPerHour -> value * METERS_PER_SECOND_TO_MILES_PER_HOUR - SpeedUnit.KilometersPerHour -> value * METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR + MeasurementSpeedUnit.MetersPerSecond -> value + MeasurementSpeedUnit.MilesPerHour -> value * METERS_PER_SECOND_TO_MILES_PER_HOUR + MeasurementSpeedUnit.KilometersPerHour -> value * METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR + MeasurementSpeedUnit.Knots -> value * METERS_PER_SECOND_TO_KNOTS } } - SpeedUnit.MilesPerHour -> { + MeasurementSpeedUnit.MilesPerHour -> { return when (converted) { - SpeedUnit.MetersPerSecond -> value / METERS_PER_SECOND_TO_MILES_PER_HOUR - SpeedUnit.MilesPerHour -> value - SpeedUnit.KilometersPerHour -> + MeasurementSpeedUnit.MetersPerSecond -> value / METERS_PER_SECOND_TO_MILES_PER_HOUR + MeasurementSpeedUnit.MilesPerHour -> value + MeasurementSpeedUnit.KilometersPerHour -> value / METERS_PER_SECOND_TO_MILES_PER_HOUR * METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR + MeasurementSpeedUnit.Knots -> + value / METERS_PER_SECOND_TO_MILES_PER_HOUR * METERS_PER_SECOND_TO_KNOTS } } - SpeedUnit.KilometersPerHour -> { + MeasurementSpeedUnit.KilometersPerHour -> { return when (converted) { - SpeedUnit.MetersPerSecond -> value / METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR - SpeedUnit.MilesPerHour -> + MeasurementSpeedUnit.MetersPerSecond -> value / METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR + MeasurementSpeedUnit.MilesPerHour -> value / METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR * METERS_PER_SECOND_TO_MILES_PER_HOUR - SpeedUnit.KilometersPerHour -> value + MeasurementSpeedUnit.KilometersPerHour -> value + MeasurementSpeedUnit.Knots -> + value / METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR * METERS_PER_SECOND_TO_KNOTS + } + } + MeasurementSpeedUnit.Knots -> { + return when (converted) { + MeasurementSpeedUnit.MetersPerSecond -> value / METERS_PER_SECOND_TO_KNOTS + MeasurementSpeedUnit.MilesPerHour -> + value / METERS_PER_SECOND_TO_KNOTS * METERS_PER_SECOND_TO_MILES_PER_HOUR + MeasurementSpeedUnit.KilometersPerHour -> + value / METERS_PER_SECOND_TO_KNOTS * METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR + MeasurementSpeedUnit.Knots -> value } } } 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 9933f490..e62a45e4 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 @@ -4,6 +4,10 @@ import androidx.lifecycle.ViewModel import com.stadiamaps.ferrostar.core.NavigationState import com.stadiamaps.ferrostar.core.NavigationUiState import com.stadiamaps.ferrostar.core.NavigationViewModel +import com.stadiamaps.ferrostar.core.annotation.AnnotationWrapper +import com.stadiamaps.ferrostar.core.annotation.Speed as SpeedLimit +import com.stadiamaps.ferrostar.core.annotation.SpeedUnit +import com.stadiamaps.ferrostar.core.annotation.valhalla.ValhallaOSRMExtendedAnnotation import java.time.Instant import kotlinx.coroutines.flow.StateFlow import uniffi.ferrostar.CourseOverGround @@ -28,6 +32,16 @@ fun UserLocation.Companion.pedestrianExample(): UserLocation { speed = Speed(1.0, 1.0)) } +fun AnnotationWrapper.Companion.pedestrianExample(): + AnnotationWrapper { + return AnnotationWrapper( + ValhallaOSRMExtendedAnnotation( + speedLimit = SpeedLimit.Value(40.0, SpeedUnit.KILOMETERS_PER_HOUR), + speed = 1.0, + distance = 1.0, + duration = 1.0)) +} + /** Mocked example for UI testing. */ fun NavigationState.Companion.pedestrianExample(): NavigationState { return NavigationState( @@ -67,7 +81,8 @@ fun NavigationUiState.Companion.pedestrianExample(): NavigationUiState = NavigationState.pedestrianExample(), false, UserLocation.pedestrianExample(), - UserLocation.pedestrianExample()) + UserLocation.pedestrianExample(), + AnnotationWrapper.pedestrianExample()) class MockNavigationViewModel(override val navigationUiState: StateFlow) : ViewModel(), NavigationViewModel { 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 2a721934..6c0d519d 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 @@ -1,14 +1,12 @@ package com.stadiamaps.ferrostar import android.Manifest +import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -16,30 +14,31 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat import com.mapbox.mapboxsdk.geometry.LatLng import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.rememberSaveableMapViewCamera import com.maplibre.compose.symbols.Circle import com.stadiamaps.autocomplete.center import com.stadiamaps.ferrostar.composeui.config.NavigationViewComponentBuilder +import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.composeui.config.withCustomOverlayView +import com.stadiamaps.ferrostar.composeui.config.withSpeedLimitStyle import com.stadiamaps.ferrostar.composeui.runtime.KeepScreenOnDisposableEffect -import com.stadiamaps.ferrostar.core.AndroidSystemLocationProvider -import com.stadiamaps.ferrostar.core.LocationProvider -import com.stadiamaps.ferrostar.googleplayservices.FusedLocationProvider +import com.stadiamaps.ferrostar.composeui.views.components.speedlimit.SignageStyle import com.stadiamaps.ferrostar.maplibreui.views.DynamicallyOrientingNavigationView import kotlin.math.min -import kotlinx.coroutines.launch @Composable fun DemoNavigationScene( savedInstanceState: Bundle?, - locationProvider: LocationProvider = AppModule.locationProvider, viewModel: DemoNavigationViewModel = AppModule.viewModel ) { // Keeps the screen on at consistent brightness while this Composable is in the view hierarchy. KeepScreenOnDisposableEffect() + val context = LocalContext.current val scope = rememberCoroutineScope() // Get location permissions. @@ -64,12 +63,7 @@ fun DemoNavigationScene( permissions -> when { permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { - val vm = viewModel - if ((locationProvider is AndroidSystemLocationProvider || - locationProvider is FusedLocationProvider)) { - // Activate location updates in the view model - vm.startLocationUpdates(locationProvider) - } + viewModel.startLocationUpdates() } permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { // TODO: Probably alert the user that this is unusable for navigation @@ -83,17 +77,12 @@ fun DemoNavigationScene( // FIXME: This is restarting navigation every time the screen is rotated. LaunchedEffect(savedInstanceState) { - // Request all permissions - permissionsLauncher.launch(allPermissions) - } - - // For smart casting - val loc = navigationUiState.location - if (loc == null) { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Text("Waiting to acquire your GPS location...", modifier = Modifier.padding(innerPadding)) + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED) { + viewModel.startLocationUpdates() + } else { + permissionsLauncher.launch(allPermissions) } - return } // Set up the map! @@ -106,16 +95,19 @@ fun DemoNavigationScene( // Snapping works well for most motor vehicle navigation. // Other travel modes though, such as walking, may not want snapping. snapUserLocationToRoute = false, + config = VisualNavigationViewConfig.Default().withSpeedLimitStyle(SignageStyle.MUTCD), views = NavigationViewComponentBuilder.Default() .withCustomOverlayView( customOverlayView = { modifier -> - AutocompleteOverlay( - modifier = modifier, - scope = scope, - isNavigating = navigationUiState.isNavigating(), - locationProvider = locationProvider, - loc = loc) + navigationUiState.location?.let { loc -> + AutocompleteOverlay( + modifier = modifier, + scope = scope, + isNavigating = navigationUiState.isNavigating(), + locationProvider = viewModel.locationProvider, + loc = loc) + } }), onTapExit = { viewModel.stopNavigation() }) { uiState -> // Trivial, if silly example of how to add your own overlay layers. 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 94e5d0ca..e4984965 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 @@ -19,56 +19,55 @@ 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 class DemoNavigationViewModel( // This is a simple example, but these would typically be dependency injected val ferrostarCore: FerrostarCore = AppModule.ferrostarCore, + val locationProvider: LocationProvider = AppModule.locationProvider, annotationPublisher: AnnotationPublisher<*> = valhallaExtendedOSRMAnnotationPublisher() ) : DefaultNavigationViewModel(ferrostarCore, annotationPublisher), LocationUpdateListener { private val locationStateFlow = MutableStateFlow(null) private val executor = Executors.newSingleThreadScheduledExecutor() - private val muteState: StateFlow = - ferrostarCore.spokenInstructionObserver?.muteState ?: MutableStateFlow(null) - - fun startLocationUpdates(locationProvider: LocationProvider) { + fun startLocationUpdates() { locationStateFlow.update { locationProvider.lastLocation } locationProvider.addListener(this, executor) } - fun stopLocationUpdates(locationProvider: LocationProvider) { + fun stopLocationUpdates() { locationProvider.removeListener(this) } + // Here's an example of injecting a custom location into the navigation UI state when isNavigating + // is false. override val navigationUiState: 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) + combine(super.navigationUiState, locationStateFlow) { a, b -> Pair(a, b) } + .map { (uiState, location) -> + if (uiState.isNavigating()) { + uiState } else { - // TODO: Heading - NavigationUiState( - userLocation, null, null, null, null, null, null, false, null, null, null, null) + uiState.copy(location = location) } } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(), - // TODO: Heading initialValue = NavigationUiState( - null, null, null, null, null, null, null, false, null, null, null, null)) + null, + null, + null, + null, + null, + null, + null, + false, + null, + null, + null, + null, + null)) override fun toggleMute() { val spokenInstructionObserver = ferrostarCore.spokenInstructionObserver diff --git a/apple/Sources/FerrostarSwiftUI/Views/SpeedLimit/SpeedLimitView.swift b/apple/Sources/FerrostarSwiftUI/Views/SpeedLimit/SpeedLimitView.swift index ad65b0c7..1af6f0ce 100644 --- a/apple/Sources/FerrostarSwiftUI/Views/SpeedLimit/SpeedLimitView.swift +++ b/apple/Sources/FerrostarSwiftUI/Views/SpeedLimit/SpeedLimitView.swift @@ -22,7 +22,7 @@ public struct SpeedLimitView: View { public init( speedLimit: Measurement, - signageStyle: SignageStyle = .viennaConvention, // Change the default once we have a better solution. + signageStyle: SignageStyle, valueFormatter: NumberFormatter = DefaultFormatters.speedFormatter, unitFormatter: MeasurementFormatter = DefaultFormatters.speedWithUnitsFormatter ) { @@ -60,9 +60,9 @@ public struct SpeedLimitView: View { #Preview { VStack { - SpeedLimitView(speedLimit: .init(value: 24.5, unit: .metersPerSecond)) + SpeedLimitView(speedLimit: .init(value: 24.5, unit: .metersPerSecond), signageStyle: .viennaConvention) - SpeedLimitView(speedLimit: .init(value: 27.8, unit: .metersPerSecond)) + SpeedLimitView(speedLimit: .init(value: 27.8, unit: .metersPerSecond), signageStyle: .viennaConvention) .environment(\.locale, .init(identifier: "fr_FR")) } .padding() diff --git a/apple/Tests/FerrostarSwiftUITests/Views/SpeedLimitViewTests.swift b/apple/Tests/FerrostarSwiftUITests/Views/SpeedLimitViewTests.swift index c0281988..983a4884 100644 --- a/apple/Tests/FerrostarSwiftUITests/Views/SpeedLimitViewTests.swift +++ b/apple/Tests/FerrostarSwiftUITests/Views/SpeedLimitViewTests.swift @@ -44,11 +44,11 @@ final class SpeedLimitViewTests: XCTestCase { func assertLocalizedSpeedLimitViews() { assertView { - SpeedLimitView(speedLimit: .init(value: 24.5, unit: .metersPerSecond)) + SpeedLimitView(speedLimit: .init(value: 24.5, unit: .metersPerSecond), signageStyle: .mutcdStyle) } assertView { - SpeedLimitView(speedLimit: .init(value: 27.8, unit: .metersPerSecond)) + SpeedLimitView(speedLimit: .init(value: 27.8, unit: .metersPerSecond), signageStyle: .viennaConvention) .environment(\.locale, .init(identifier: "fr_FR")) } } @@ -97,11 +97,11 @@ final class SpeedLimitViewTests: XCTestCase { func assertLocalizedSpeedLimitViews_darkMode() { assertView(colorScheme: .dark) { - SpeedLimitView(speedLimit: .init(value: 24.5, unit: .metersPerSecond)) + SpeedLimitView(speedLimit: .init(value: 24.5, unit: .metersPerSecond), signageStyle: .mutcdStyle) } assertView(colorScheme: .dark) { - SpeedLimitView(speedLimit: .init(value: 27.8, unit: .metersPerSecond)) + SpeedLimitView(speedLimit: .init(value: 27.8, unit: .metersPerSecond), signageStyle: .viennaConvention) .environment(\.locale, .init(identifier: "fr_FR")) } }