diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt index a78638a43..3e5414b13 100644 --- a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt +++ b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt @@ -24,9 +24,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.viewinterop.AndroidView -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.model.MetricsViewModel import org.meshtastic.feature.map.addCopyright import org.meshtastic.feature.map.addPolyline import org.meshtastic.feature.map.addPositionMarkers @@ -39,20 +37,16 @@ import org.osmdroid.util.GeoPoint private const val DEG_D = 1e-7 @Composable -fun NodeMapScreen( - metricsViewModel: MetricsViewModel = hiltViewModel(), - nodeMapViewModel: NodeMapViewModel = hiltViewModel(), - onNavigateUp: () -> Unit, -) { +fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit) { val density = LocalDensity.current - val state by metricsViewModel.state.collectAsStateWithLifecycle() - val geoPoints = state.positionLogs.map { GeoPoint(it.latitudeI * DEG_D, it.longitudeI * DEG_D) } + val positionLogs by nodeMapViewModel.positionLogs.collectAsStateWithLifecycle() + val geoPoints = positionLogs.map { GeoPoint(it.latitudeI * DEG_D, it.longitudeI * DEG_D) } val cameraView = remember { BoundingBox.fromGeoPoints(geoPoints) } val mapView = rememberMapViewWithLifecycle( applicationId = nodeMapViewModel.applicationId, box = cameraView, - tileSource = metricsViewModel.tileSource, + tileSource = nodeMapViewModel.tileSource, ) AndroidView( @@ -64,7 +58,7 @@ fun NodeMapScreen( map.addScaleBarOverlay(density) map.addPolyline(density, geoPoints) {} - map.addPositionMarkers(state.positionLogs) {} + map.addPositionMarkers(positionLogs) {} }, ) } diff --git a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt index b420d276e..c92e4867d 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt @@ -21,32 +21,24 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.model.MetricsViewModel import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.feature.map.MapView import org.meshtastic.feature.map.node.NodeMapViewModel @Composable -fun NodeMapScreen( - metricsViewModel: MetricsViewModel = hiltViewModel(), - nodeMapViewModel: NodeMapViewModel = hiltViewModel(), - onNavigateUp: () -> Unit, -) { - val state by metricsViewModel.state.collectAsState() - val positions = state.positionLogs - val destNum = state.node?.num - val ourNodeInfo by nodeMapViewModel.ourNodeInfo.collectAsStateWithLifecycle() +fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit) { + val node by nodeMapViewModel.node.collectAsStateWithLifecycle() + val positions by nodeMapViewModel.positionLogs.collectAsStateWithLifecycle() + val destNum = node?.num Scaffold( topBar = { MainAppBar( - title = state.node?.user?.longName ?: "", - ourNode = ourNodeInfo, + title = node?.user?.longName ?: "", + ourNode = null, showNodeChip = false, canNavigateUp = true, onNavigateUp = onNavigateUp, diff --git a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt index bad83f8d2..748b265b8 100644 --- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt @@ -62,6 +62,7 @@ import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.prefs.map.MapPrefs +import org.meshtastic.core.proto.toPosition import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.strings.R @@ -190,12 +191,6 @@ enum class TimeFrame(val seconds: Long, @StringRes val strRes: Int) { private fun MeshPacket.hasValidSignal(): Boolean = rxTime > 0 && (rxSnr != 0f && rxRssi != 0) && (hopStart > 0 && hopStart - hopLimit == 0) -private fun MeshPacket.toPosition(): Position? = if (!decoded.wantResponse) { - runCatching { Position.parseFrom(decoded.payload) }.getOrNull() -} else { - null -} - @Suppress("LongParameterList") @HiltViewModel class MetricsViewModel diff --git a/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt index c2064776e..7611bf388 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt @@ -57,6 +57,7 @@ import org.meshtastic.core.navigation.NodeDetailRoutes import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.navigation.Route import org.meshtastic.core.strings.R +import org.meshtastic.feature.map.node.NodeMapViewModel fun NavGraphBuilder.nodesGraph(navController: NavHostController) { navigation(startDestination = NodesRoutes.Nodes) { @@ -90,6 +91,21 @@ fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController) { ) } + composable( + deepLinks = + listOf( + navDeepLink(basePath = "$DEEP_LINK_BASE_URI/node/{destNum}/node_map"), + navDeepLink(basePath = "$DEEP_LINK_BASE_URI/node/node_map"), + ), + ) { backStackEntry -> + val parentGraphBackStackEntry = + remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) } + NodeMapScreen( + hiltViewModel(parentGraphBackStackEntry), + onNavigateUp = navController::navigateUp, + ) + } + NodeDetailRoute.entries.forEach { entry -> when (entry.route) { is NodeDetailRoutes.DeviceMetrics -> @@ -98,12 +114,6 @@ fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController) { entry, entry.screenComposable, ) - is NodeDetailRoutes.NodeMap -> - addNodeDetailScreenComposable( - navController, - entry, - entry.screenComposable, - ) is NodeDetailRoutes.PositionLog -> addNodeDetailScreenComposable( navController, @@ -199,12 +209,6 @@ enum class NodeDetailRoute( Icons.Default.Router, { metricsVM, onNavigateUp -> DeviceMetricsScreen(metricsVM, onNavigateUp) }, ), - NODE_MAP( - R.string.node_map, - NodeDetailRoutes.NodeMap, - Icons.Default.LocationOn, - { metricsVM, onNavigateUp -> NodeMapScreen(metricsVM, onNavigateUp = onNavigateUp) }, - ), POSITION_LOG( R.string.position_log, NodeDetailRoutes.PositionLog, diff --git a/core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt b/core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt index 5b6d9bab6..b62e31f95 100644 --- a/core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt +++ b/core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt @@ -20,6 +20,8 @@ package org.meshtastic.core.proto import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.MeshProtos.MeshPacket +import com.geeksville.mesh.MeshProtos.Position import java.text.DateFormat import kotlin.time.Duration.Companion.days @@ -38,3 +40,9 @@ fun MeshProtos.Position.formatPositionTime(dateFormat: DateFormat): String { } return timeText } + +fun MeshPacket.toPosition(): Position? = if (!decoded.wantResponse) { + runCatching { Position.parseFrom(decoded.payload) }.getOrNull() +} else { + null +} diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index cba6a7b29..5d8ccf228 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(projects.core.database) implementation(projects.core.datastore) implementation(projects.core.model) + implementation(projects.core.navigation) implementation(projects.core.prefs) implementation(projects.core.proto) implementation(projects.core.service) diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt index 3b29d6257..f9f25c56a 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt @@ -17,18 +17,64 @@ package org.meshtastic.feature.map.node +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.geeksville.mesh.MeshProtos.Position +import com.geeksville.mesh.Portnums.PortNum import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.toList import org.meshtastic.core.common.BuildConfigProvider +import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.navigation.NodesRoutes +import org.meshtastic.core.prefs.map.MapPrefs +import org.meshtastic.core.proto.toPosition +import org.meshtastic.feature.map.model.CustomTileSource import javax.inject.Inject @HiltViewModel -class NodeMapViewModel @Inject constructor(nodeRepository: NodeRepository, buildConfigProvider: BuildConfigProvider) : - ViewModel() { - val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo +class NodeMapViewModel +@Inject +constructor( + savedStateHandle: SavedStateHandle, + nodeRepository: NodeRepository, + meshLogRepository: MeshLogRepository, + buildConfigProvider: BuildConfigProvider, + private val mapPrefs: MapPrefs, +) : ViewModel() { + private val destNum = savedStateHandle.toRoute().destNum + + val node = + nodeRepository.nodeDBbyNum + .mapLatest { it[destNum] } + .distinctUntilChanged() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), null) val applicationId = buildConfigProvider.applicationId + + val positionLogs: StateFlow> = + meshLogRepository + .getMeshPacketsFrom(destNum!!, PortNum.POSITION_APP_VALUE) + .map { packets -> + packets + .mapNotNull { it.toPosition() } + .asFlow() + .distinctUntilChanged { old, new -> + old.time == new.time || (old.latitudeI == new.latitudeI && old.longitudeI == new.longitudeI) + } + .toList() + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), emptyList()) + + val tileSource + get() = CustomTileSource.getTileSource(mapPrefs.mapStyle) }