Skip to content

Commit

Permalink
refactor: add more granular Position precision options
Browse files Browse the repository at this point in the history
closes #1186
  • Loading branch information
andrekir committed Aug 18, 2024
1 parent 7e0cfff commit ddad40a
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 85 deletions.
Original file line number Diff line number Diff line change
@@ -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

private enum class PositionPrecision(val value: Int) {
HIGH_PRECISION(32),
MED_PRECISION(16),
LOW_PRECISION(11),
DISABLED(0),
;
}
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 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(
Expand All @@ -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)
)
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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 = {
Expand All @@ -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() })
Expand Down
80 changes: 54 additions & 26 deletions app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit ddad40a

Please sign in to comment.