Skip to content

Commit

Permalink
feat: node map position history log (#1384)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrekir authored Nov 6, 2024
1 parent a8c810b commit 227c65f
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 44 deletions.
5 changes: 5 additions & 0 deletions app/src/main/java/com/geeksville/mesh/ui/NavGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
109 changes: 65 additions & 44 deletions app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,7 +101,6 @@ fun NodeDetailScreen(
}
}

@Suppress("LongMethod")
@Composable
private fun NodeDetailList(
node: NodeEntity,
Expand Down Expand Up @@ -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")
}

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")
}
PreferenceCategory(stringResource(id = R.string.logs))
LogNavigationList(metricsState, onNavigate)
}

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")
}
}
}
}
Expand Down Expand Up @@ -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,
Expand Down
84 changes: 84 additions & 0 deletions app/src/main/java/com/geeksville/mesh/ui/components/NodeMap.kt
Original file line number Diff line number Diff line change
@@ -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) {}
}
)
}
67 changes: 67 additions & 0 deletions app/src/main/java/com/geeksville/mesh/util/MapViewExtensions.kt
Original file line number Diff line number Diff line change
@@ -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

/**
Expand Down Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,12 @@
<string name="rssi_definition">Received Signal Strength Indicator, a measurement used to determine the power level being received by the antenna. A higher RSSI value generally indicates a stronger and more stable connection.</string>
<string name="iaq_definition">(Indoor Air Quality) relative scale IAQ value as measured by Bosch BME680. Value Range 0–500.</string>
<string name="device_metrics_log">Device Metrics Log</string>
<string name="node_map">Node Map</string>
<string name="position_log">Position Log</string>
<string name="env_metrics_log">Environment Metrics Log</string>
<string name="sig_metrics_log">Signal Metrics Log</string>
<string name="administration">Administration</string>
<string name="remote_admin">Remote Administration</string>
<string name="bad">Bad</string>
<string name="fair">Fair</string>
<string name="good">Good</string>
Expand Down
4 changes: 4 additions & 0 deletions config/detekt/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,12 @@
<ID>MagicNumber:MapFragment.kt$12F</ID>
<ID>MagicNumber:MapFragment.kt$1e-7</ID>
<ID>MagicNumber:MapFragment.kt$&lt;no name provided&gt;$1e7</ID>
<ID>MagicNumber:MapViewExtensions.kt$1e-5</ID>
<ID>MagicNumber:MapViewExtensions.kt$1e-7</ID>
<ID>MagicNumber:MapViewExtensions.kt$3.0f</ID>
<ID>MagicNumber:MapViewExtensions.kt$40f</ID>
<ID>MagicNumber:MapViewExtensions.kt$60f</ID>
<ID>MagicNumber:MapViewExtensions.kt$80f</ID>
<ID>MagicNumber:MarkerWithLabel.kt$MarkerWithLabel$3</ID>
<ID>MagicNumber:MeshService.kt$MeshService$0xffffffff</ID>
<ID>MagicNumber:MeshService.kt$MeshService$100</ID>
Expand Down

0 comments on commit 227c65f

Please sign in to comment.