mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-04 12:47:53 -04:00
Decouple NodeMapScreen from MetricsViewModel (#3323)
Co-authored-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -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) {}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<NodesRoutes.NodesGraph>(startDestination = NodesRoutes.Nodes) {
|
||||
@@ -90,6 +91,21 @@ fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController) {
|
||||
)
|
||||
}
|
||||
|
||||
composable<NodeDetailRoutes.NodeMap>(
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<NodeDetailRoutes.NodeMap>(basePath = "$DEEP_LINK_BASE_URI/node/{destNum}/node_map"),
|
||||
navDeepLink<NodeDetailRoutes.NodeMap>(basePath = "$DEEP_LINK_BASE_URI/node/node_map"),
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val parentGraphBackStackEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
|
||||
NodeMapScreen(
|
||||
hiltViewModel<NodeMapViewModel>(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<NodeDetailRoutes.NodeMap>(
|
||||
navController,
|
||||
entry,
|
||||
entry.screenComposable,
|
||||
)
|
||||
is NodeDetailRoutes.PositionLog ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.PositionLog>(
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Node?> = nodeRepository.ourNodeInfo
|
||||
class NodeMapViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
nodeRepository: NodeRepository,
|
||||
meshLogRepository: MeshLogRepository,
|
||||
buildConfigProvider: BuildConfigProvider,
|
||||
private val mapPrefs: MapPrefs,
|
||||
) : ViewModel() {
|
||||
private val destNum = savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum
|
||||
|
||||
val node =
|
||||
nodeRepository.nodeDBbyNum
|
||||
.mapLatest { it[destNum] }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), null)
|
||||
|
||||
val applicationId = buildConfigProvider.applicationId
|
||||
|
||||
val positionLogs: StateFlow<List<Position>> =
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user