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"))
}
}