mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Traceroute map position snapshots (#4035)
Signed-off-by: Jord <650645+DivineOmega@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
03fd2bf9ba
commit
9833795864
17 changed files with 1195 additions and 44 deletions
|
|
@ -132,6 +132,7 @@ import org.meshtastic.feature.map.component.MapButton
|
|||
import org.meshtastic.feature.map.model.CustomTileSource
|
||||
import org.meshtastic.feature.map.model.MarkerWithLabel
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.proto.MeshProtos.Position
|
||||
import org.meshtastic.proto.MeshProtos.Waypoint
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.waypoint
|
||||
|
|
@ -231,6 +232,7 @@ fun MapView(
|
|||
mapViewModel: MapViewModel = hiltViewModel(),
|
||||
navigateToNodeDetails: (Int) -> Unit,
|
||||
tracerouteOverlay: TracerouteOverlay? = null,
|
||||
tracerouteNodePositions: Map<Int, Position> = emptyMap(),
|
||||
onTracerouteMappableCountChanged: (shown: Int, total: Int) -> Unit = { _, _ -> },
|
||||
) {
|
||||
var mapFilterExpanded by remember { mutableStateOf(false) }
|
||||
|
|
@ -334,14 +336,17 @@ fun MapView(
|
|||
|
||||
val nodes by mapViewModel.nodes.collectAsStateWithLifecycle()
|
||||
val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap())
|
||||
val nodeLookup = remember(nodes) { nodes.filter { it.validPosition != null }.associateBy { it.num } }
|
||||
val overlayNodeNums = remember(tracerouteOverlay) { tracerouteOverlay?.relatedNodeNums ?: emptySet() }
|
||||
val nodesForMarkers =
|
||||
if (tracerouteOverlay != null) {
|
||||
nodes.filter { overlayNodeNums.contains(it.num) }
|
||||
} else {
|
||||
nodes
|
||||
val tracerouteSelection =
|
||||
remember(tracerouteOverlay, tracerouteNodePositions, nodes) {
|
||||
mapViewModel.tracerouteNodeSelection(
|
||||
tracerouteOverlay = tracerouteOverlay,
|
||||
tracerouteNodePositions = tracerouteNodePositions,
|
||||
nodes = nodes,
|
||||
)
|
||||
}
|
||||
val overlayNodeNums = tracerouteSelection.overlayNodeNums
|
||||
val nodeLookup = tracerouteSelection.nodeLookup
|
||||
val nodesForMarkers = tracerouteSelection.nodesForMarkers
|
||||
val tracerouteForwardPoints =
|
||||
remember(tracerouteOverlay, nodeLookup) {
|
||||
tracerouteOverlay?.forwardRoute?.mapNotNull {
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ fun MapView(
|
|||
focusedNodeNum: Int? = null,
|
||||
nodeTracks: List<Position>? = null,
|
||||
tracerouteOverlay: TracerouteOverlay? = null,
|
||||
tracerouteNodePositions: Map<Int, Position> = emptyMap(),
|
||||
onTracerouteMappableCountChanged: (shown: Int, total: Int) -> Unit = { _, _ -> },
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -262,7 +263,14 @@ fun MapView(
|
|||
.collectAsStateWithLifecycle(listOf())
|
||||
val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap())
|
||||
val displayableWaypoints = waypoints.values.mapNotNull { it.data.waypoint }
|
||||
val overlayNodeNums = remember(tracerouteOverlay) { tracerouteOverlay?.relatedNodeNums ?: emptySet() }
|
||||
val tracerouteSelection =
|
||||
remember(tracerouteOverlay, tracerouteNodePositions, allNodes) {
|
||||
mapViewModel.tracerouteNodeSelection(
|
||||
tracerouteOverlay = tracerouteOverlay,
|
||||
tracerouteNodePositions = tracerouteNodePositions,
|
||||
nodes = allNodes,
|
||||
)
|
||||
}
|
||||
|
||||
val filteredNodes =
|
||||
allNodes
|
||||
|
|
@ -275,7 +283,7 @@ fun MapView(
|
|||
|
||||
val displayNodes =
|
||||
if (tracerouteOverlay != null) {
|
||||
allNodes.filter { overlayNodeNums.contains(it.num) }
|
||||
tracerouteSelection.nodesForMarkers
|
||||
} else {
|
||||
filteredNodes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import org.meshtastic.core.strings.one_day
|
|||
import org.meshtastic.core.strings.one_hour
|
||||
import org.meshtastic.core.strings.two_days
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
@ -68,7 +69,7 @@ sealed class LastHeardFilter(val seconds: Long, val label: StringResource) {
|
|||
@Suppress("TooManyFunctions")
|
||||
abstract class BaseMapViewModel(
|
||||
protected val mapPrefs: MapPrefs,
|
||||
nodeRepository: NodeRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val packetRepository: PacketRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
) : ViewModel() {
|
||||
|
|
@ -118,6 +119,12 @@ abstract class BaseMapViewModel(
|
|||
|
||||
val ourNodeInfo: StateFlow<Node?> = nodeRepository.ourNodeInfo
|
||||
|
||||
fun getNodeByNum(nodeNum: Int): Node? = nodeRepository.nodeDBbyNum.value[nodeNum]
|
||||
|
||||
fun getUser(nodeNum: Int): MeshProtos.User = nodeRepository.getUser(nodeNum)
|
||||
|
||||
fun getNodeOrFallback(nodeNum: Int): Node = getNodeByNum(nodeNum) ?: Node(num = nodeNum, user = getUser(nodeNum))
|
||||
|
||||
val isConnected =
|
||||
serviceRepository.connectionState.map { it.isConnected() }.stateInWhileSubscribed(initialValue = false)
|
||||
|
||||
|
|
@ -196,3 +203,47 @@ abstract class BaseMapViewModel(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class TracerouteNodeSelection(
|
||||
val overlayNodeNums: Set<Int>,
|
||||
val nodesForMarkers: List<Node>,
|
||||
val nodeLookup: Map<Int, Node>,
|
||||
)
|
||||
|
||||
fun BaseMapViewModel.tracerouteNodeSelection(
|
||||
tracerouteOverlay: TracerouteOverlay?,
|
||||
tracerouteNodePositions: Map<Int, MeshProtos.Position>,
|
||||
nodes: List<Node>,
|
||||
): TracerouteNodeSelection {
|
||||
val overlayNodeNums = tracerouteOverlay?.relatedNodeNums ?: emptySet()
|
||||
val tracerouteSnapshotNodes =
|
||||
if (tracerouteOverlay == null || tracerouteNodePositions.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
tracerouteNodePositions.map { (nodeNum, position) -> getNodeOrFallback(nodeNum).copy(position = position) }
|
||||
}
|
||||
|
||||
val nodesForMarkers =
|
||||
if (tracerouteOverlay != null) {
|
||||
if (tracerouteSnapshotNodes.isNotEmpty()) {
|
||||
tracerouteSnapshotNodes.filter { overlayNodeNums.contains(it.num) }
|
||||
} else {
|
||||
nodes.filter { overlayNodeNums.contains(it.num) }
|
||||
}
|
||||
} else {
|
||||
nodes
|
||||
}
|
||||
|
||||
val nodesForLookup =
|
||||
if (tracerouteSnapshotNodes.isNotEmpty()) {
|
||||
tracerouteSnapshotNodes
|
||||
} else {
|
||||
nodes.filter { it.validPosition != null }
|
||||
}
|
||||
|
||||
return TracerouteNodeSelection(
|
||||
overlayNodeNums = overlayNodeNums,
|
||||
nodesForMarkers = nodesForMarkers,
|
||||
nodeLookup = nodesForLookup.associateBy { it.num },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
|||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
|
|
@ -87,6 +88,7 @@ constructor(
|
|||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val tracerouteSnapshotRepository: TracerouteSnapshotRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
) : ViewModel() {
|
||||
|
|
@ -146,6 +148,8 @@ constructor(
|
|||
return overlay
|
||||
}
|
||||
|
||||
fun tracerouteSnapshotPositions(logUuid: String) = tracerouteSnapshotRepository.getSnapshotPositions(logUuid)
|
||||
|
||||
fun clearTracerouteResponse() = serviceRepository.clearTracerouteResponse()
|
||||
|
||||
fun tracerouteMapAvailability(forwardRoute: List<Int>, returnRoute: List<Int>): TracerouteMapAvailability =
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
import org.meshtastic.core.model.getTracerouteResponse
|
||||
import org.meshtastic.core.model.toMessageRes
|
||||
|
|
@ -89,7 +90,12 @@ import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
|
|||
import org.meshtastic.proto.MeshProtos
|
||||
import java.text.DateFormat
|
||||
|
||||
private data class TracerouteDialog(val message: AnnotatedString, val requestId: Int, val overlay: TracerouteOverlay?)
|
||||
private data class TracerouteDialog(
|
||||
val message: AnnotatedString,
|
||||
val requestId: Int,
|
||||
val responseLogUuid: String,
|
||||
val overlay: TracerouteOverlay?,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -98,7 +104,7 @@ fun TracerouteLogScreen(
|
|||
modifier: Modifier = Modifier,
|
||||
viewModel: MetricsViewModel = hiltViewModel(),
|
||||
onNavigateUp: () -> Unit,
|
||||
onViewOnMap: (requestId: Int) -> Unit = {},
|
||||
onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit = { _, _ -> },
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) }
|
||||
|
|
@ -185,10 +191,12 @@ fun TracerouteLogScreen(
|
|||
AnnotatedString(it)
|
||||
}
|
||||
dialogMessage?.let {
|
||||
val responseLogUuid = result?.uuid ?: return@combinedClickable
|
||||
showDialog =
|
||||
TracerouteDialog(
|
||||
message = it,
|
||||
requestId = log.fromRadio.packet.id,
|
||||
responseLogUuid = responseLogUuid,
|
||||
overlay = overlay,
|
||||
)
|
||||
}
|
||||
|
|
@ -211,23 +219,34 @@ private fun TracerouteLogDialogs(
|
|||
dialog: TracerouteDialog?,
|
||||
errorMessageRes: StringResource?,
|
||||
viewModel: MetricsViewModel,
|
||||
onViewOnMap: (requestId: Int) -> Unit,
|
||||
onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit,
|
||||
onShowErrorMessageRes: (StringResource) -> Unit,
|
||||
onDismissDialog: () -> Unit,
|
||||
onDismissError: () -> Unit,
|
||||
) {
|
||||
dialog?.let { dialogState ->
|
||||
val snapshotPositionsFlow =
|
||||
remember(dialogState.responseLogUuid) { viewModel.tracerouteSnapshotPositions(dialogState.responseLogUuid) }
|
||||
val snapshotPositions by snapshotPositionsFlow.collectAsStateWithLifecycle(emptyMap<Int, MeshProtos.Position>())
|
||||
SimpleAlertDialog(
|
||||
title = Res.string.traceroute,
|
||||
text = { SelectionContainer { Text(text = dialogState.message) } },
|
||||
confirmText = stringResource(Res.string.view_on_map),
|
||||
onConfirm = {
|
||||
val positionedNodeNums =
|
||||
if (snapshotPositions.isNotEmpty()) {
|
||||
snapshotPositions.keys
|
||||
} else {
|
||||
viewModel.positionedNodeNums()
|
||||
}
|
||||
val availability =
|
||||
viewModel.tracerouteMapAvailability(
|
||||
evaluateTracerouteMapAvailability(
|
||||
forwardRoute = dialogState.overlay?.forwardRoute.orEmpty(),
|
||||
returnRoute = dialogState.overlay?.returnRoute.orEmpty(),
|
||||
positionedNodeNums = positionedNodeNums,
|
||||
)
|
||||
availability.toMessageRes()?.let(onShowErrorMessageRes) ?: onViewOnMap(dialogState.requestId)
|
||||
availability.toMessageRes()?.let(onShowErrorMessageRes)
|
||||
?: onViewOnMap(dialogState.requestId, dialogState.responseLogUuid)
|
||||
onDismissDialog()
|
||||
},
|
||||
onDismiss = onDismissDialog,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
import org.meshtastic.core.strings.Res
|
||||
|
|
@ -54,37 +55,59 @@ import org.meshtastic.core.ui.component.MainAppBar
|
|||
import org.meshtastic.core.ui.theme.TracerouteColors
|
||||
import org.meshtastic.feature.map.MapView
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
|
||||
@Composable
|
||||
fun TracerouteMapScreen(
|
||||
metricsViewModel: MetricsViewModel = hiltViewModel(),
|
||||
requestId: Int,
|
||||
logUuid: String? = null,
|
||||
onNavigateUp: () -> Unit,
|
||||
) {
|
||||
val state by metricsViewModel.state.collectAsStateWithLifecycle()
|
||||
val nodeTitle = state.node?.user?.longName ?: stringResource(Res.string.traceroute)
|
||||
val routeDiscovery =
|
||||
state.tracerouteResults
|
||||
.find { it.fromRadio.packet.decoded.requestId == requestId }
|
||||
?.fromRadio
|
||||
?.packet
|
||||
?.fullRouteDiscovery
|
||||
val snapshotPositions by
|
||||
remember(logUuid) {
|
||||
logUuid?.let(metricsViewModel::tracerouteSnapshotPositions)
|
||||
?: flowOf(emptyMap<Int, MeshProtos.Position>())
|
||||
}
|
||||
.collectAsStateWithLifecycle(emptyMap<Int, MeshProtos.Position>())
|
||||
val tracerouteResult =
|
||||
if (logUuid != null) {
|
||||
state.tracerouteResults.find { it.uuid == logUuid }
|
||||
} else {
|
||||
state.tracerouteResults.find { it.fromRadio.packet.decoded.requestId == requestId }
|
||||
}
|
||||
val routeDiscovery = tracerouteResult?.fromRadio?.packet?.fullRouteDiscovery
|
||||
val overlayFromLogs =
|
||||
remember(routeDiscovery) {
|
||||
routeDiscovery?.let {
|
||||
TracerouteOverlay(requestId = requestId, forwardRoute = it.routeList, returnRoute = it.routeBackList)
|
||||
}
|
||||
remember(routeDiscovery, requestId) {
|
||||
routeDiscovery?.let { TracerouteOverlay(requestId, it.routeList, it.routeBackList) }
|
||||
}
|
||||
val overlayFromService = remember(requestId) { metricsViewModel.getTracerouteOverlay(requestId) }
|
||||
val overlay = overlayFromLogs ?: overlayFromService
|
||||
var tracerouteNodesShown by remember { mutableStateOf(0) }
|
||||
var tracerouteNodesTotal by remember { mutableStateOf(0) }
|
||||
LaunchedEffect(Unit) { metricsViewModel.clearTracerouteResponse() }
|
||||
|
||||
TracerouteMapScaffold(
|
||||
title = state.node?.user?.longName ?: stringResource(Res.string.traceroute),
|
||||
overlay = overlay,
|
||||
snapshotPositions = snapshotPositions,
|
||||
onNavigateUp = onNavigateUp,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TracerouteMapScaffold(
|
||||
title: String,
|
||||
overlay: TracerouteOverlay?,
|
||||
snapshotPositions: Map<Int, MeshProtos.Position>,
|
||||
onNavigateUp: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var tracerouteNodesShown by remember { mutableStateOf(0) }
|
||||
var tracerouteNodesTotal by remember { mutableStateOf(0) }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = nodeTitle,
|
||||
title = title,
|
||||
ourNode = null,
|
||||
showNodeChip = false,
|
||||
canNavigateUp = true,
|
||||
|
|
@ -94,10 +117,11 @@ fun TracerouteMapScreen(
|
|||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
|
||||
Box(modifier = modifier.fillMaxSize().padding(paddingValues)) {
|
||||
MapView(
|
||||
navigateToNodeDetails = {},
|
||||
tracerouteOverlay = overlay,
|
||||
tracerouteNodePositions = snapshotPositions,
|
||||
onTracerouteMappableCountChanged = { shown, total ->
|
||||
tracerouteNodesShown = shown
|
||||
tracerouteNodesTotal = total
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue