refactor(NodeItem): replace NodeInfo with NodeEntity

This commit is contained in:
andrekir 2024-09-21 15:45:10 -03:00 committed by Andre K
parent 89a3171b58
commit 83dc389d6d
12 changed files with 373 additions and 160 deletions

View file

@ -140,7 +140,9 @@ class NodeInfoDaoTest {
@Test
fun testSortByDistance() = runBlocking {
val nodes = getNodes(sort = NodeSortOption.DISTANCE)
val sortedNodes = nodes.sortedBy { it.distance(ourNode) }
val sortedNodes = nodes.sortedWith( // nodes with invalid (null) positions at the end
compareBy<NodeEntity> { it.validPosition == null }.thenBy { it.distance(ourNode) }
)
assertEquals(sortedNodes, nodes)
}

View file

@ -89,8 +89,56 @@ data class NodeEntity(
longitude = degD(p.longitudeI)
}
private fun hasValidPosition(): Boolean {
return latitude != 0.0 && longitude != 0.0 &&
(latitude >= -90 && latitude <= 90.0) &&
(longitude >= -180 && longitude <= 180)
}
val validPosition: MeshProtos.Position? get() = position.takeIf { hasValidPosition() }
// @return distance in meters to some other node (or null if unknown)
fun distance(o: NodeEntity) = latLongToMeter(latitude, longitude, o.latitude, o.longitude)
fun distance(o: NodeEntity): Int? {
return if (validPosition == null || o.validPosition == null) null
else latLongToMeter(latitude, longitude, o.latitude, o.longitude).toInt()
}
private fun TelemetryProtos.EnvironmentMetrics.getDisplayString(isFahrenheit: Boolean): String {
val temp = if (temperature != 0f) {
if (isFahrenheit) {
val fahrenheit = temperature * 1.8F + 32
"%.1f°F".format(fahrenheit)
} else {
"%.1f°C".format(temperature)
}
} else null
val humidity = if (relativeHumidity != 0f) "%.0f%%".format(relativeHumidity) else null
val pressure = if (barometricPressure != 0f) "%.1fhPa".format(barometricPressure) else null
val gas = if (gasResistance != 0f) "%.0fMΩ".format(gasResistance) else null
val voltage = if (this.voltage != 0f) "%.2fV".format(this.voltage) else null
val current = if (current != 0f) "%.1fmA".format(current) else null
val iaq = if (iaq != 0) "IAQ: $iaq" else null
return listOfNotNull(
temp,
humidity,
pressure,
gas,
voltage,
current,
iaq,
).joinToString(" ")
}
private fun PaxcountProtos.Paxcount.getDisplayString() =
"PAX: ${ble + wifi} (B:$ble/W:$wifi)".takeIf { ble != 0 && wifi != 0 }
fun getTelemetryString(isFahrenheit: Boolean = false): String {
return listOfNotNull(
paxcounter.getDisplayString(),
environmentMetrics.getDisplayString(isFahrenheit)
).joinToString(" ")
}
/**
* true if the device was heard from recently

View file

@ -3,10 +3,8 @@ package com.geeksville.mesh.model
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.toNodeInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -26,8 +24,8 @@ class NodeDB @Inject constructor(
val myNodeInfo: StateFlow<MyNodeInfo?> get() = _myNodeInfo
// our node info
private val _ourNodeInfo = MutableStateFlow<NodeInfo?>(null)
val ourNodeInfo: StateFlow<NodeInfo?> get() = _ourNodeInfo
private val _ourNodeInfo = MutableStateFlow<NodeEntity?>(null)
val ourNodeInfo: StateFlow<NodeEntity?> get() = _ourNodeInfo
// The unique userId of our node
private val _myId = MutableStateFlow<String?>(null)
@ -47,7 +45,7 @@ class NodeDB @Inject constructor(
nodeInfoDao.nodeDBbyNum().onEach {
_nodeDBbyNum.value = it
val ourNodeInfo = it.values.firstOrNull()?.toNodeInfo()
val ourNodeInfo = it.values.firstOrNull()
_ourNodeInfo.value = ourNodeInfo
_myId.value = ourNodeInfo?.user?.id
}.launchIn(processLifecycle.coroutineScope)

View file

@ -240,7 +240,7 @@ class UIViewModel @Inject constructor(
// hardware info about our local device (can be null)
val myNodeInfo: StateFlow<MyNodeInfo?> get() = nodeDB.myNodeInfo
val ourNodeInfo: StateFlow<NodeInfo?> get() = nodeDB.ourNodeInfo
val ourNodeInfo: StateFlow<NodeEntity?> get() = nodeDB.ourNodeInfo
// FIXME only used in MapFragment
val initialNodes get() = nodeDB.nodeDBbyNum.value.values.map { it.toNodeInfo() }
@ -545,10 +545,15 @@ class UIViewModel @Inject constructor(
}
}
fun setOwner(user: MeshUser) {
fun setOwner(name: String) {
val user = ourNodeInfo.value?.user?.copy {
longName = name
shortName = getInitials(name)
} ?: return
try {
// Note: we use ?. here because we might be running in the emulator
meshService?.setOwner(user)
meshService?.setRemoteOwner(myNodeNum ?: return, user.toByteArray())
} catch (ex: RemoteException) {
errormsg("Can't set username on device, is device offline? ${ex.message}")
}

View file

@ -3,7 +3,6 @@ package com.geeksville.mesh.ui
import android.content.ActivityNotFoundException
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -11,91 +10,79 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.Position
import com.geeksville.mesh.R
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.GpsCoordinateFormat
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.ui.theme.AppTheme
import com.geeksville.mesh.ui.theme.HyperlinkBlue
import com.geeksville.mesh.util.GPSFormat
import java.net.URLEncoder
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LinkedCoordinates(
modifier: Modifier = Modifier,
position: Position?,
latitude: Double,
longitude: Double,
format: Int,
nodeName: String?
nodeName: String,
) {
if (position?.isValid() == true) {
val uriHandler = LocalUriHandler.current
val style = SpanStyle(
color = HyperlinkBlue,
fontSize = MaterialTheme.typography.button.fontSize,
textDecoration = TextDecoration.Underline
val uriHandler = LocalUriHandler.current
val style = SpanStyle(
color = HyperlinkBlue,
fontSize = MaterialTheme.typography.button.fontSize,
textDecoration = TextDecoration.Underline
)
val annotatedString = buildAnnotatedString {
pushStringAnnotation(
tag = "gps",
// URI scheme is defined at:
// https://developer.android.com/guide/components/intents-common#Maps
annotation = "geo:0,0?q=${latitude},${longitude}&z=17&label=${
URLEncoder.encode(nodeName, "utf-8")
}"
)
val name = nodeName ?: stringResource(id = R.string.unknown_username)
val annotatedString = buildAnnotatedString {
pushStringAnnotation(
tag = "gps",
// URI scheme is defined at:
// https://developer.android.com/guide/components/intents-common#Maps
annotation = "geo:0,0?q=${position.latitude},${position.longitude}&z=17&label=${
URLEncoder.encode(name, "utf-8")
}"
)
withStyle(style = style) {
append(position.gpsString(format))
withStyle(style = style) {
val gpsString = when (format) {
GpsCoordinateFormat.DEC_VALUE -> GPSFormat.toDEC(latitude, longitude)
GpsCoordinateFormat.DMS_VALUE -> GPSFormat.toDMS(latitude, longitude)
GpsCoordinateFormat.UTM_VALUE -> GPSFormat.toUTM(latitude, longitude)
GpsCoordinateFormat.MGRS_VALUE -> GPSFormat.toMGRS(latitude, longitude)
else -> GPSFormat.toDEC(latitude, longitude)
}
pop()
append(gpsString)
}
val clipboardManager: ClipboardManager = LocalClipboardManager.current
Text(
modifier = modifier.combinedClickable(
onClick = {
annotatedString.getStringAnnotations(
tag = "gps",
start = 0,
end = annotatedString.length
).firstOrNull()?.let {
try {
uriHandler.openUri(it.item)
} catch (ex: ActivityNotFoundException) {
debug("No application found: $ex")
}
pop()
}
val clipboardManager: ClipboardManager = LocalClipboardManager.current
Text(
modifier = modifier.combinedClickable(
onClick = {
annotatedString.getStringAnnotations(
tag = "gps",
start = 0,
end = annotatedString.length
).firstOrNull()?.let {
try {
uriHandler.openUri(it.item)
} catch (ex: ActivityNotFoundException) {
debug("No application found: $ex")
}
},
onLongClick = {
clipboardManager.setText(annotatedString)
debug("Copied to clipboard")
}
),
text = annotatedString
)
} else {
// Placeholder for ConstraintLayoutReference; renders no visible content
Box(modifier = modifier)
}
}
@Composable
@Preview
fun LinkedCoordinatesSimplePreview() {
AppTheme {
LinkedCoordinates(
position = Position(37.7749, -122.4194, 0),
format = 1,
nodeName = "Test Node Name"
)
}
},
onLongClick = {
clipboardManager.setText(annotatedString)
debug("Copied to clipboard")
}
),
text = annotatedString
)
}
@PreviewLightDark
@ -105,7 +92,8 @@ fun LinkedCoordinatesPreview(
) {
AppTheme {
LinkedCoordinates(
position = Position(37.7749, -122.4194, 0),
latitude = 37.7749,
longitude = -122.4194,
format = format,
nodeName = "Test Node Name"
)
@ -115,4 +103,4 @@ fun LinkedCoordinatesPreview(
class GPSFormatPreviewParameterProvider : PreviewParameterProvider<Int> {
override val values: Sequence<Int>
get() = sequenceOf(0, 1, 2)
}
}

View file

@ -1,8 +1,5 @@
@file:Suppress(
"FunctionNaming",
"LongMethod",
"LongParameterList",
"DestructuringDeclarationWithTooManyEntries",
"MagicNumber",
"CyclomaticComplexMethod",
)
@ -53,20 +50,22 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.ui.compose.ElevationInfo
import com.geeksville.mesh.ui.compose.SatelliteCountInfo
import com.geeksville.mesh.ui.preview.NodeInfoPreviewParameterProvider
import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider
import com.geeksville.mesh.ui.theme.AppTheme
import com.geeksville.mesh.util.metersIn
import com.geeksville.mesh.util.toDistanceString
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun NodeItem(
thisNodeInfo: NodeInfo?,
thatNodeInfo: NodeInfo,
thisNode: NodeEntity?,
thatNode: NodeEntity,
gpsFormat: Int,
distanceUnits: Int,
tempInFahrenheit: Boolean,
@ -75,28 +74,29 @@ fun NodeItem(
blinking: Boolean = false,
expanded: Boolean = false,
currentTimeMillis: Long,
hasPublicKey: Boolean = false,
) {
val isUnknownUser = thatNodeInfo.user?.hwModel == MeshProtos.HardwareModel.UNSET
val hasPublicKey = !thatNode.user.publicKey.isEmpty
val isUnknownUser = thatNode.user.hwModel == MeshProtos.HardwareModel.UNSET
val unknownShortName = stringResource(id = R.string.unknown_node_short_name)
val longName = thatNodeInfo.user?.longName ?: stringResource(id = R.string.unknown_username)
val longName = thatNode.user.longName.ifEmpty { stringResource(id = R.string.unknown_username) }
val nodeName = if (hasPublicKey) "🔒 $longName" else longName
val isThisNode = thisNodeInfo?.num == thatNodeInfo.num
val distance = thisNodeInfo?.distanceStr(thatNodeInfo, distanceUnits)
val (textColor, nodeColor) = thatNodeInfo.colors
val isThisNode = thisNode?.num == thatNode.num
val distance = thisNode?.distance(thatNode)?.let {
val system = DisplayConfig.DisplayUnits.forNumber(distanceUnits)
if (it == 0) null else it.toDistanceString(system)
}
val (textColor, nodeColor) = thatNode.colors
val position = thatNodeInfo.position
val hwInfoString = thatNodeInfo.user?.hwModelString ?: MeshProtos.HardwareModel.UNSET.name
val roleName =
if (isUnknownUser) {
DeviceConfig.Role.UNRECOGNIZED.name
} else {
thatNodeInfo.user?.role?.let { role ->
DeviceConfig.Role.forNumber(role)?.name
} ?: DeviceConfig.Role.UNRECOGNIZED.name
}
val nodeId = thatNodeInfo.user?.id ?: "???"
val hwInfoString = thatNode.user.hwModel.let { hwModel ->
if (hwModel == MeshProtos.HardwareModel.UNSET) MeshProtos.HardwareModel.UNSET.name
else hwModel.name.replace('_', '-').replace('p', '.').lowercase()
}
val roleName = if (isUnknownUser) {
DeviceConfig.Role.UNRECOGNIZED.name
} else {
thatNode.user.role.name
}
val nodeId = thatNode.user.id.ifEmpty { "???" }
val highlight = Color(0x33FFFFFF)
val bgColor by animateColorAsState(
@ -153,7 +153,7 @@ fun NodeItem(
content = {
Text(
modifier = Modifier.fillMaxWidth(),
text = thatNodeInfo.user?.shortName ?: unknownShortName,
text = thatNode.user.shortName.ifEmpty { unknownShortName },
fontWeight = FontWeight.Normal,
fontSize = MaterialTheme.typography.button.fontSize,
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
@ -163,14 +163,14 @@ fun NodeItem(
)
Text(
modifier = Modifier.weight(1f),
text = nodeName,
text = if (hasPublicKey) "🔒 $longName" else longName,
style = style,
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
softWrap = true,
)
LastHeardInfo(
lastHeard = thatNodeInfo.lastHeard,
lastHeard = thatNode.lastHeard,
currentTimeMillis = currentTimeMillis
)
}
@ -188,8 +188,8 @@ fun NodeItem(
Spacer(modifier = Modifier.width(16.dp))
}
BatteryInfo(
batteryLevel = thatNodeInfo.batteryLevel,
voltage = thatNodeInfo.voltage
batteryLevel = thatNode.batteryLevel,
voltage = thatNode.voltage
)
}
Spacer(modifier = Modifier.height(4.dp))
@ -198,11 +198,11 @@ fun NodeItem(
horizontalArrangement = Arrangement.SpaceBetween
) {
signalInfo(
nodeInfo = thatNodeInfo,
node = thatNode,
isThisNode = isThisNode
)
if (position?.isValid() == true) {
val satCount = position.satellitesInView
thatNode.validPosition?.let { position ->
val satCount = position.satsInView
if (satCount > 0) {
SatelliteCountInfo(
satCount = satCount
@ -215,9 +215,10 @@ fun NodeItem(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
thatNodeInfo.environmentMetrics?.getDisplayString(tempInFahrenheit)?.let { envMetrics ->
val telemetryString = thatNode.getTelemetryString(tempInFahrenheit)
if (telemetryString.isNotEmpty()) {
Text(
text = envMetrics,
text = telemetryString,
color = MaterialTheme.colors.onSurface,
fontSize = MaterialTheme.typography.button.fontSize
)
@ -233,16 +234,19 @@ fun NodeItem(
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
DisableSelection {
LinkedCoordinates(
position = position,
format = gpsFormat,
nodeName = nodeName
)
thatNode.validPosition?.let {
DisableSelection {
LinkedCoordinates(
latitude = thatNode.latitude,
longitude = thatNode.longitude,
format = gpsFormat,
nodeName = longName
)
}
}
val system =
ConfigProtos.Config.DisplayConfig.DisplayUnits.forNumber(distanceUnits)
if (position?.isValid() == true) {
thatNode.validPosition?.let { position ->
val altitude = position.altitude.metersIn(system)
val elevationSuffix = stringResource(id = R.string.elevation_suffix)
ElevationInfo(
@ -266,13 +270,15 @@ fun NodeItem(
modifier = Modifier.weight(1f),
text = roleName,
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.button.fontSize
fontSize = MaterialTheme.typography.button.fontSize,
style = style,
)
Text(
modifier = Modifier.weight(1f),
text = nodeId,
textAlign = TextAlign.End,
fontSize = MaterialTheme.typography.button.fontSize
fontSize = MaterialTheme.typography.button.fontSize,
style = style,
)
}
}
@ -286,11 +292,11 @@ fun NodeItem(
@Preview(showBackground = false)
fun NodeInfoSimplePreview() {
AppTheme {
val thisNodeInfo = NodeInfoPreviewParameterProvider().values.first()
val thatNodeInfo = NodeInfoPreviewParameterProvider().values.last()
val thisNode = NodeEntityPreviewParameterProvider().values.first()
val thatNode = NodeEntityPreviewParameterProvider().values.last()
NodeItem(
thisNodeInfo = thisNodeInfo,
thatNodeInfo = thatNodeInfo,
thisNode = thisNode,
thatNode = thatNode,
1,
0,
true,
@ -305,19 +311,19 @@ fun NodeInfoSimplePreview() {
uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES,
)
fun NodeInfoPreview(
@PreviewParameter(NodeInfoPreviewParameterProvider::class)
thatNodeInfo: NodeInfo
@PreviewParameter(NodeEntityPreviewParameterProvider::class)
thatNode: NodeEntity
) {
AppTheme {
val thisNodeInfo = NodeInfoPreviewParameterProvider().values.first()
val thisNode = NodeEntityPreviewParameterProvider().values.first()
Column {
Text(
text = "Details Collapsed",
color = MaterialTheme.colors.onBackground
)
NodeItem(
thisNodeInfo = thisNodeInfo,
thatNodeInfo = thatNodeInfo,
thisNode = thisNode,
thatNode = thatNode,
gpsFormat = 0,
distanceUnits = 1,
tempInFahrenheit = true,
@ -329,8 +335,8 @@ fun NodeInfoPreview(
color = MaterialTheme.colors.onBackground
)
NodeItem(
thisNodeInfo = thisNodeInfo,
thatNodeInfo = thatNodeInfo,
thisNode = thisNode,
thatNode = thatNode,
gpsFormat = 0,
distanceUnits = 1,
tempInFahrenheit = true,

View file

@ -26,7 +26,6 @@ import com.geeksville.mesh.databinding.SettingsFragmentBinding
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.getInitials
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.util.exceptionToSnackbar
@ -218,10 +217,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
binding.usernameEditText.onEditorAction(EditorInfo.IME_ACTION_DONE) {
debug("received IME_ACTION_DONE")
val n = binding.usernameEditText.text.toString().trim()
model.ourNodeInfo.value?.user?.let {
val user = it.copy(longName = n, shortName = getInitials(n))
if (n.isNotEmpty()) model.setOwner(user)
}
if (n.isNotEmpty()) model.setOwner(n)
requireActivity().hideKeyboard()
}

View file

@ -8,31 +8,31 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.preview.NodeInfoPreviewParameterProvider
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider
import com.geeksville.mesh.ui.theme.AppTheme
@Composable
fun signalInfo(
modifier: Modifier = Modifier,
nodeInfo: NodeInfo,
node: NodeEntity,
isThisNode: Boolean
): Boolean {
val text = if (isThisNode) {
stringResource(R.string.channel_air_util).format(
nodeInfo.deviceMetrics?.channelUtilization,
nodeInfo.deviceMetrics?.airUtilTx
node.deviceMetrics.channelUtilization,
node.deviceMetrics.airUtilTx
)
} else {
buildList {
if (nodeInfo.channel > 0) add("ch:${nodeInfo.channel}")
if (nodeInfo.hopsAway == 0) {
if (nodeInfo.snr < 100F && nodeInfo.rssi < 0) {
add("RSSI: %d SNR: %.1f".format(nodeInfo.rssi, nodeInfo.snr))
if (node.channel > 0) add("ch:${node.channel}")
if (node.hopsAway == 0) {
if (node.snr < 100F && node.rssi < 0) {
add("RSSI: %d SNR: %.1f".format(node.rssi, node.snr))
}
} else {
add("%s: %d".format(stringResource(R.string.hops_away), nodeInfo.hopsAway))
add("%s: %d".format(stringResource(R.string.hops_away), node.hopsAway))
}
}.joinToString(" ")
}
@ -54,15 +54,12 @@ fun signalInfo(
fun SignalInfoSimplePreview() {
AppTheme {
signalInfo(
nodeInfo = NodeInfo(
node = NodeEntity(
num = 1,
position = null,
lastHeard = 0,
channel = 0,
snr = 12.5F,
rssi = -42,
deviceMetrics = null,
user = null,
hopsAway = 0
),
isThisNode = false
@ -73,12 +70,12 @@ fun SignalInfoSimplePreview() {
@PreviewLightDark
@Composable
fun SignalInfoPreview(
@PreviewParameter(NodeInfoPreviewParameterProvider::class)
nodeInfo: NodeInfo
@PreviewParameter(NodeEntityPreviewParameterProvider::class)
node: NodeEntity
) {
AppTheme {
signalInfo(
nodeInfo = nodeInfo,
node = node,
isThisNode = false
)
}
@ -87,12 +84,12 @@ fun SignalInfoPreview(
@Composable
@PreviewLightDark
fun SignalInfoSelfPreview(
@PreviewParameter(NodeInfoPreviewParameterProvider::class)
nodeInfo: NodeInfo
@PreviewParameter(NodeEntityPreviewParameterProvider::class)
node: NodeEntity
) {
AppTheme {
signalInfo(
nodeInfo = nodeInfo,
node = node,
isThisNode = true
)
}

View file

@ -133,7 +133,7 @@ fun NodesScreen(
val state by model.nodesUiState.collectAsStateWithLifecycle()
val nodes by model.nodeList.collectAsStateWithLifecycle()
val ourNodeInfo by model.ourNodeInfo.collectAsStateWithLifecycle()
val ourNode by model.ourNodeInfo.collectAsStateWithLifecycle()
val listState = rememberLazyListState()
val focusedNode by model.focusedNode.collectAsStateWithLifecycle()
@ -172,8 +172,8 @@ fun NodesScreen(
items(nodes, key = { it.num }) { node ->
val nodeInfo = node.toNodeInfo()
NodeItem(
thisNodeInfo = ourNodeInfo,
thatNodeInfo = nodeInfo,
thisNode = ourNode,
thatNode = node,
gpsFormat = state.gpsFormat,
distanceUnits = state.distanceUnits,
tempInFahrenheit = state.tempInFahrenheit,
@ -185,7 +185,6 @@ fun NodesScreen(
blinking = nodeInfo == focusedNode,
expanded = state.showDetails,
currentTimeMillis = currentTimeMillis,
hasPublicKey = !node.user.publicKey.isEmpty
)
}
}

View file

@ -318,7 +318,7 @@ fun MapView(
fun MapView.onNodesChanged(nodes: Collection<NodeInfo>): List<MarkerWithLabel> {
val nodesWithPosition = nodes.filter { it.validPosition != null }
val ourNode = model.ourNodeInfo.value
val ourNode = model.ourNodeInfo.value?.toNodeInfo()
val gpsFormat = model.config.display.gpsFormat.number
val displayUnits = model.config.display.units.number
return nodesWithPosition.map { node ->

View file

@ -0,0 +1,146 @@
package com.geeksville.mesh.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.DeviceMetrics.Companion.currentTime
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.deviceMetrics
import com.geeksville.mesh.environmentMetrics
import com.geeksville.mesh.paxcount
import com.geeksville.mesh.position
import com.geeksville.mesh.telemetry
import com.geeksville.mesh.user
import kotlin.random.Random
class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity> {
val mickeyMouse = NodeEntity(
num = 1955,
user = user {
id = "mickeyMouseId"
longName = "Mickey Mouse"
shortName = "MM"
hwModel = MeshProtos.HardwareModel.TBEAM
},
longName = "Mickey Mouse",
shortName = "MM",
position = position {
latitudeI = 338125110
longitudeI = -1179189760
altitude = 138
satsInView = 4
},
latitude = 33.812511,
longitude = -117.918976,
lastHeard = currentTime(),
channel = 0,
snr = 12.5F,
rssi = -42,
deviceTelemetry = telemetry {
deviceMetrics = deviceMetrics {
channelUtilization = 2.4F
airUtilTx = 3.5F
batteryLevel = 85
voltage = 3.7F
uptimeSeconds = 3600
}
},
hopsAway = 0
)
private val minnieMouse = mickeyMouse.copy(
num = Random.nextInt(),
user = user {
longName = "Minnie Mouse"
shortName = "MiMo"
id = "minnieMouseId"
hwModel = MeshProtos.HardwareModel.HELTEC_V3
},
longName = "Minnie Mouse",
shortName = "MiMo",
snr = 12.5F,
rssi = -42,
position = position {},
latitude = 0.0,
longitude = 0.0,
hopsAway = 1
)
private val donaldDuck = NodeEntity(
num = Random.nextInt(),
position = position {
latitudeI = 338052347
longitudeI = -1179208460
altitude = 121
satsInView = 66
},
latitude = 33.8052347,
longitude = -117.9208460,
lastHeard = currentTime() - 300,
channel = 0,
snr = 12.5F,
rssi = -42,
deviceTelemetry = telemetry {
deviceMetrics = deviceMetrics {
channelUtilization = 2.4F
airUtilTx = 3.5F
batteryLevel = 85
voltage = 3.7F
uptimeSeconds = 3600
}
},
user = user {
id = "donaldDuckId"
longName = "Donald Duck, the Grand Duck of the Ducks"
shortName = "DoDu"
hwModel = MeshProtos.HardwareModel.HELTEC_V3
},
longName = "Donald Duck, the Grand Duck of the Ducks",
shortName = "DoDu",
environmentTelemetry = telemetry {
environmentMetrics = environmentMetrics {
temperature = 28.0F
relativeHumidity = 50.0F
barometricPressure = 1013.25F
gasResistance = 0.0F
voltage = 3.7F
current = 0.0F
iaq = 100
}
},
paxcounter = paxcount {
wifi = 30
ble = 39
uptime = 420
},
hopsAway = 2
)
private val unknown = donaldDuck.copy(
user = user {
id = "myId"
longName = "Meshtastic myId"
shortName = "myId"
hwModel = MeshProtos.HardwareModel.UNSET
},
longName = "Meshtastic myId",
shortName = null,
environmentTelemetry = telemetry {
environmentMetrics = environmentMetrics {}
},
paxcounter = paxcount {},
)
private val almostNothing = NodeEntity(
num = Random.nextInt(),
)
override val values: Sequence<NodeEntity>
get() = sequenceOf(
mickeyMouse, // "this" node
unknown,
almostNothing,
minnieMouse,
donaldDuck
)
}

View file

@ -58,6 +58,34 @@ object GPSFormat {
MGRS.northing
)
}
fun toDEC(latitude: Double, longitude: Double): String {
return "%.5f %.5f".format(latitude, longitude).replace(",", ".")
}
fun toDMS(latitude: Double, longitude: Double): String {
val lat = degreesToDMS(latitude, true)
val lon = degreesToDMS(longitude, false)
fun string(a: Array<String>) = "%s°%s'%.5s\"%s".format(a[0], a[1], a[2], a[3])
return string(lat) + " " + string(lon)
}
fun toUTM(latitude: Double, longitude: Double): String {
val UTM = UTM.from(Point.point(longitude, latitude))
return "%s%s %.6s %.7s".format(UTM.zone, UTM.toMGRS().band, UTM.easting, UTM.northing)
}
fun toMGRS(latitude: Double, longitude: Double): String {
val MGRS = MGRS.from(Point.point(longitude, latitude))
return "%s%s %s%s %05d %05d".format(
MGRS.zone,
MGRS.band,
MGRS.column,
MGRS.row,
MGRS.easting,
MGRS.northing
)
}
}
/**