kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat: node map position history log (#1384)
rodzic
a8c810bae2
commit
227c65f191
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -269,8 +269,12 @@
|
|||
<ID>MagicNumber:MapFragment.kt$12F</ID>
|
||||
<ID>MagicNumber:MapFragment.kt$1e-7</ID>
|
||||
<ID>MagicNumber:MapFragment.kt$<no name provided>$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>
|
||||
|
|
Ładowanie…
Reference in New Issue