feat: node map position history log (#1384)

This commit is contained in:
Andre K 2024-11-06 11:00:38 -03:00 committed by GitHub
parent a8c810bae2
commit 227c65f191
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 228 additions and 44 deletions

View file

@ -61,6 +61,7 @@ import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DeviceMetricsScreen
import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen
import com.geeksville.mesh.ui.components.NodeMapScreen
import com.geeksville.mesh.ui.components.PositionLogScreen
import com.geeksville.mesh.ui.components.SignalMetricsScreen
import com.geeksville.mesh.ui.components.TracerouteLogScreen
@ -251,6 +252,10 @@ fun NavGraph(
val parentEntry = remember { navController.getBackStackEntry("NodeDetails") }
DeviceMetricsScreen(hiltViewModel<MetricsViewModel>(parentEntry))
}
composable("NodeMap") {
val parentEntry = remember { navController.getBackStackEntry("NodeDetails") }
NodeMapScreen(hiltViewModel<MetricsViewModel>(parentEntry))
}
composable("PositionLog") {
val parentEntry = remember { navController.getBackStackEntry("NodeDetails") }
PositionLogScreen(hiltViewModel<MetricsViewModel>(parentEntry))

View file

@ -34,6 +34,7 @@ import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.KeyOff
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Map
import androidx.compose.material.icons.filled.Numbers
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Power
@ -100,7 +101,6 @@ fun NodeDetailScreen(
}
}
@Suppress("LongMethod")
@Composable
private fun NodeDetailList(
node: NodeEntity,
@ -135,50 +135,20 @@ private fun NodeDetailList(
}
item {
NavCard(
title = stringResource(R.string.device_metrics_log),
icon = Icons.Default.ChargingStation,
enabled = metricsState.hasDeviceMetrics()
) {
onNavigate("DeviceMetrics")
}
PreferenceCategory(stringResource(id = R.string.logs))
LogNavigationList(metricsState, onNavigate)
}
NavCard(
title = stringResource(R.string.position_log),
icon = Icons.Default.LocationOn,
enabled = metricsState.hasPositionLogs()
) { onNavigate("PositionLog") }
NavCard(
title = stringResource(R.string.env_metrics_log),
icon = Icons.Default.Thermostat,
enabled = metricsState.hasEnvironmentMetrics()
) {
onNavigate("EnvironmentMetrics")
}
NavCard(
title = stringResource(R.string.sig_metrics_log),
icon = Icons.Default.SignalCellularAlt,
enabled = metricsState.hasSignalMetrics()
) {
onNavigate("SignalMetrics")
}
NavCard(
title = stringResource(R.string.traceroute_log),
icon = Icons.Default.Route,
enabled = metricsState.hasTracerouteLogs()
) {
onNavigate("TracerouteList")
}
NavCard(
title = "Remote Administration",
icon = Icons.Default.Settings,
enabled = !metricsState.isManaged
) {
onNavigate("RadioConfig")
if (!metricsState.isManaged) {
item {
PreferenceCategory(stringResource(id = R.string.administration))
NavCard(
title = stringResource(id = R.string.remote_admin),
icon = Icons.Default.Settings,
enabled = true
) {
onNavigate("RadioConfig")
}
}
}
}
@ -260,6 +230,57 @@ private fun NodeDetailsContent(node: NodeEntity) {
)
}
@Composable
fun LogNavigationList(state: MetricsState, onNavigate: (String) -> Unit) {
NavCard(
title = stringResource(R.string.device_metrics_log),
icon = Icons.Default.ChargingStation,
enabled = state.hasDeviceMetrics()
) {
onNavigate("DeviceMetrics")
}
NavCard(
title = stringResource(R.string.node_map),
icon = Icons.Default.Map,
enabled = state.hasPositionLogs()
) {
onNavigate("NodeMap")
}
NavCard(
title = stringResource(R.string.position_log),
icon = Icons.Default.LocationOn,
enabled = state.hasPositionLogs()
) {
onNavigate("PositionLog")
}
NavCard(
title = stringResource(R.string.env_metrics_log),
icon = Icons.Default.Thermostat,
enabled = state.hasEnvironmentMetrics()
) {
onNavigate("EnvironmentMetrics")
}
NavCard(
title = stringResource(R.string.sig_metrics_log),
icon = Icons.Default.SignalCellularAlt,
enabled = state.hasSignalMetrics()
) {
onNavigate("SignalMetrics")
}
NavCard(
title = stringResource(R.string.traceroute_log),
icon = Icons.Default.Route,
enabled = state.hasTracerouteLogs()
) {
onNavigate("TracerouteList")
}
}
@Composable
private fun InfoCard(
icon: ImageVector,

View file

@ -0,0 +1,84 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.ui.map.rememberMapViewWithLifecycle
import com.geeksville.mesh.util.addCopyright
import com.geeksville.mesh.util.addPositionMarkers
import com.geeksville.mesh.util.addPolyline
import com.geeksville.mesh.util.addScaleBarOverlay
import com.geeksville.mesh.util.requiredZoomLevel
import org.osmdroid.config.Configuration
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
private const val DegD = 1e-7
@Composable
fun NodeMapScreen(
viewModel: MetricsViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val density = LocalDensity.current
val mapView = rememberMapViewWithLifecycle(context)
val state by viewModel.state.collectAsStateWithLifecycle()
val geoPoints = state.positionLogs.map { GeoPoint(it.latitudeI * DegD, it.longitudeI * DegD) }
var savedCenter by rememberSaveable(stateSaver = Saver(
save = { mapOf("latitude" to it.latitude, "longitude" to it.longitude) },
restore = { GeoPoint(it["latitude"] ?: 0.0, it["longitude"] ?: .0) }
)) {
val box = BoundingBox.fromGeoPoints(geoPoints)
mutableStateOf(GeoPoint(box.centerLatitude, box.centerLongitude))
}
var savedZoom by rememberSaveable {
val box = BoundingBox.fromGeoPoints(geoPoints)
mutableDoubleStateOf(box.requiredZoomLevel())
}
LifecycleStartEffect(true) {
onStopOrDispose {
savedCenter = mapView.projection.currentCenter
savedZoom = mapView.zoomLevelDouble
}
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = {
mapView.apply {
Configuration.getInstance().userAgentValue = BuildConfig.APPLICATION_ID
setMultiTouchControls(true)
isTilesScaledToDpi = true
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
controller.setCenter(savedCenter)
controller.setZoom(savedZoom)
}
},
update = { map ->
map.overlays.clear()
map.addCopyright()
map.addScaleBarOverlay(density)
map.addPolyline(density, geoPoints) {}
map.addPositionMarkers(state.positionLogs) {}
}
)
}

View file

@ -1,18 +1,26 @@
package com.geeksville.mesh.util
import android.graphics.Color
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.graphics.Typeface
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R
import org.osmdroid.events.DelayedMapListener
import org.osmdroid.events.MapListener
import org.osmdroid.events.ScrollEvent
import org.osmdroid.events.ZoomEvent
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.CopyrightOverlay
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.ScaleBarOverlay
import org.osmdroid.views.overlay.advancedpolyline.MonochromaticPaintList
import org.osmdroid.views.overlay.gridlines.LatLonGridlineOverlay2
/**
@ -81,3 +89,62 @@ fun MapView.addMapEventListener(onEvent: () -> Unit) {
}
}, INACTIVITY_DELAY_MILLIS))
}
fun MapView.addPolyline(
density: Density,
geoPoints: List<GeoPoint>,
onClick: () -> Unit
): Polyline {
val polyline = Polyline(this).apply {
val borderPaint = Paint().apply {
color = Color.BLACK
isAntiAlias = true
strokeWidth = with(density) { 10.dp.toPx() }
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
pathEffect = DashPathEffect(floatArrayOf(80f, 60f), 0f)
}
outlinePaintLists.add(MonochromaticPaintList(borderPaint))
val fillPaint = Paint().apply {
color = Color.WHITE
isAntiAlias = true
strokeWidth = with(density) { 6.dp.toPx() }
style = Paint.Style.FILL_AND_STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
pathEffect = DashPathEffect(floatArrayOf(80f, 60f), 0f)
}
outlinePaintLists.add(MonochromaticPaintList(fillPaint))
setPoints(geoPoints)
setOnClickListener { _, _, _ ->
onClick()
true
}
}
overlays.add(polyline)
return polyline
}
fun MapView.addPositionMarkers(
positions: List<MeshProtos.Position>,
onClick: () -> Unit
): List<Marker> {
val navIcon = ContextCompat.getDrawable(context, R.drawable.ic_map_navigation_24)
val markers = positions.map {
Marker(this).apply {
icon = navIcon
rotation = (it.groundTrack * 1e-5).toFloat()
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
position = GeoPoint(it.latitudeI * 1e-7, it.longitudeI * 1e-7)
setOnMarkerClickListener { _, _ ->
onClick()
true
}
}
}
overlays.addAll(markers)
return markers
}