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:
Jord 2025-12-18 14:14:03 +00:00 committed by GitHub
parent 03fd2bf9ba
commit 9833795864
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1195 additions and 44 deletions

View file

@ -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 {

View file

@ -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
}

View file

@ -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 },
)
}