refactor: add more granular Position precision options

closes #1186
This commit is contained in:
andrekir 2024-08-18 08:32:20 -03:00
parent 7e0cfff67b
commit ddad40a969
4 changed files with 169 additions and 84 deletions

View file

@ -1,17 +1,33 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Slider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.util.DistanceUnit
import com.geeksville.mesh.util.toDistanceString
import java.util.Locale
import kotlin.math.pow
private enum class PositionPrecision(val value: Int) {
HIGH_PRECISION(32),
MED_PRECISION(16),
LOW_PRECISION(11),
DISABLED(0),
;
}
private const val PositionEnabled = 32
private const val PositionDisabled = 0
const val PositionPrecisionMin = 10
const val PositionPrecisionMax = 19
const val PositionPrecisionDefault = 13
@Suppress("MagicNumber")
fun precisionBitsToMeters(bits: Int): Double = 23905787.925008 * 0.5.pow(bits.toDouble())
@Composable
fun PositionPrecisionPreference(
@ -21,24 +37,64 @@ fun PositionPrecisionPreference(
onValueChanged: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
val useDropDown = PositionPrecision.entries.any { it.value == value }
val unit = remember { DistanceUnit.getFromLocale(Locale.getDefault()) }
if (useDropDown) {
DropDownPreference(
Column(modifier = modifier) {
SwitchPreference(
title = title,
checked = value != PositionDisabled,
enabled = enabled,
items = PositionPrecision.entries.map { it.value to it.name },
selectedItem = value,
onItemSelected = onValueChanged,
)
} else {
EditTextPreference(
title = title,
value = value,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = onValueChanged,
onCheckedChange = { enabled ->
val newValue = if (enabled) PositionEnabled else PositionDisabled
onValueChanged(newValue)
},
padding = PaddingValues(0.dp)
)
AnimatedVisibility(visible = value != PositionDisabled) {
SwitchPreference(
title = "Precise location",
checked = value == PositionEnabled,
enabled = enabled,
onCheckedChange = { enabled ->
val newValue = if (enabled) PositionEnabled else PositionPrecisionDefault
onValueChanged(newValue)
},
padding = PaddingValues(0.dp)
)
}
AnimatedVisibility(visible = value in (PositionDisabled + 1)..<PositionEnabled) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Slider(
value = value.toFloat(),
onValueChange = { onValueChanged(it.toInt()) },
enabled = enabled,
valueRange = PositionPrecisionMin.toFloat()..PositionPrecisionMax.toFloat(),
steps = PositionPrecisionMax - PositionPrecisionMin - 1,
)
val precisionMeters = precisionBitsToMeters(value).toInt()
Text(
text = precisionMeters.toDistanceString(unit),
modifier = Modifier.padding(bottom = 16.dp),
fontSize = MaterialTheme.typography.body1.fontSize,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun PositionPrecisionPreferencePreview() {
PositionPrecisionPreference(
title = "Position enabled",
value = PositionPrecisionDefault,
enabled = true,
onValueChanged = {},
modifier = Modifier.padding(horizontal = 16.dp)
)
}

View file

@ -1,5 +1,6 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -26,12 +27,13 @@ fun SwitchPreference(
enabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(horizontal = 16.dp),
) {
Row(
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.size(48.dp)
.padding(start = 16.dp, end = 16.dp),
.padding(padding),
verticalAlignment = Alignment.CenterVertically
) {
Text(

View file

@ -3,7 +3,9 @@ package com.geeksville.mesh.ui.components.config
import android.util.Base64
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
@ -16,8 +18,8 @@ import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Close
import androidx.compose.material.icons.twotone.Refresh
@ -26,7 +28,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
@ -41,11 +42,14 @@ import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PositionPrecisionPreference
import com.geeksville.mesh.ui.components.SwitchPreference
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
import com.google.protobuf.ByteString
import com.google.protobuf.kotlin.toByteString
import java.security.SecureRandom
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun EditChannelDialog(
channelSettings: ChannelProtos.ChannelSettings,
@ -133,32 +137,28 @@ fun EditChannelDialog(
},
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Uplink enabled", // TODO move to resource strings
modifier = modifier.weight(1f)
)
Switch(
checked = channelInput.uplinkEnabled,
onCheckedChange = {
channelInput = channelInput.copy { uplinkEnabled = it }
},
)
}
SwitchPreference(
title = "Uplink enabled",
checked = channelInput.uplinkEnabled,
enabled = true,
onCheckedChange = {
channelInput = channelInput.copy { uplinkEnabled = it }
},
padding = PaddingValues(0.dp)
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Downlink enabled", // TODO move to resource strings
modifier = modifier.weight(1f)
)
Switch(
checked = channelInput.downlinkEnabled,
onCheckedChange = {
channelInput = channelInput.copy { downlinkEnabled = it }
},
)
}
SwitchPreference(
title = "Downlink enabled",
checked = channelInput.downlinkEnabled,
enabled = true,
onCheckedChange = {
channelInput = channelInput.copy { downlinkEnabled = it }
},
padding = PaddingValues(0.dp)
)
PositionPrecisionPreference(
title = "Position",
title = "Position enabled",
enabled = true,
value = channelInput.moduleSettings.positionPrecision,
onValueChanged = {
@ -170,22 +170,21 @@ fun EditChannelDialog(
}
},
buttons = {
Row(
modifier = modifier.fillMaxWidth(),
FlowRow(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Button(
TextButton(
modifier = modifier
.fillMaxWidth()
.padding(start = 24.dp)
.weight(1f),
onClick = onDismissRequest
) { Text(stringResource(R.string.cancel)) }
Button(
modifier = modifier
.fillMaxWidth()
.padding(end = 24.dp)
.weight(1f),
onClick = {
onAddClick(channelInput.copy { name = channelInput.name.trim() })

View file

@ -1,46 +1,74 @@
package com.geeksville.mesh.util
import com.geeksville.mesh.ConfigProtos
import android.icu.util.LocaleData
import android.icu.util.ULocale
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import java.util.Locale
enum class DistanceUnit(
val symbol: String,
val multiplier: Float,
val system: Int
) {
METERS("m", 1F, ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE),
KILOMETERS("km", 0.001F, ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE),
FEET("ft", 3.28084F, ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE),
MILES("mi", 0.000621371F, ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE),
METER("m", multiplier = 1F, DisplayConfig.DisplayUnits.METRIC_VALUE),
KILOMETER("km", multiplier = 0.001F, DisplayConfig.DisplayUnits.METRIC_VALUE),
FOOT("ft", multiplier = 3.28084F, DisplayConfig.DisplayUnits.IMPERIAL_VALUE),
MILE("mi", multiplier = 0.000621371F, DisplayConfig.DisplayUnits.IMPERIAL_VALUE),
;
companion object {
fun getFromLocale(locale: Locale): DisplayConfig.DisplayUnits {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
when (LocaleData.getMeasurementSystem(ULocale.forLocale(locale))) {
LocaleData.MeasurementSystem.SI -> DisplayConfig.DisplayUnits.METRIC
else -> DisplayConfig.DisplayUnits.IMPERIAL
}
} else {
when (locale.country.uppercase(locale)) {
"US", "LR", "MM", "GB" -> DisplayConfig.DisplayUnits.IMPERIAL
else -> DisplayConfig.DisplayUnits.METRIC
}
}
}
}
}
fun Int.metersIn(unit: DistanceUnit): Float {
return this * unit.multiplier
}
fun Int.metersIn(system: ConfigProtos.Config.DisplayConfig.DisplayUnits): Float {
return this * when (system.number) {
ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE -> DistanceUnit.METERS.multiplier
ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FEET.multiplier
else -> throw IllegalArgumentException("Unknown distance system $system")
fun Int.metersIn(system: DisplayConfig.DisplayUnits): Float {
val unit = when (system.number) {
DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FOOT
else -> DistanceUnit.METER
}
return this.metersIn(unit)
}
fun Float.toString(unit: DistanceUnit): String {
return "%.1f %s".format(this, unit.symbol)
return if (unit in setOf(DistanceUnit.METER, DistanceUnit.FOOT)) {
"%.0f %s"
} else {
"%.1f %s"
}.format(this, unit.symbol)
}
fun Float.toString(
system: ConfigProtos.Config.DisplayConfig.DisplayUnits
): String {
return "%.1f %s".format(this,
when (system.number) {
ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE -> {
DistanceUnit.METERS.symbol
}
ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> {
DistanceUnit.FEET.symbol
}
else -> throw IllegalArgumentException("Unknown distance system $system")
},
)
}
fun Float.toString(system: DisplayConfig.DisplayUnits): String {
val unit = when (system.number) {
DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FOOT
else -> DistanceUnit.METER
}
return this.toString(unit)
}
private const val KILOMETER_THRESHOLD = 1000
private const val MILE_THRESHOLD = 1609
fun Int.toDistanceString(system: DisplayConfig.DisplayUnits): String {
val unit = if (system.number == DisplayConfig.DisplayUnits.METRIC_VALUE) {
if (this < KILOMETER_THRESHOLD) DistanceUnit.METER else DistanceUnit.KILOMETER
} else {
if (this < MILE_THRESHOLD) DistanceUnit.FOOT else DistanceUnit.MILE
}
val valueInUnit = this * unit.multiplier
return valueInUnit.toString(unit)
}