From acbae6d93d5921eb9a6851f57929e63df9857194 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 3 Aug 2024 11:08:22 -0500 Subject: [PATCH] feat: Add position precision indicator to map (#1177) --- .../mesh/repository/radio/MockInterface.kt | 3 + .../com/geeksville/mesh/ui/map/MapFragment.kt | 56 +++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt index d987f16f1..5b0dd3e60 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt @@ -9,6 +9,7 @@ import com.google.protobuf.ByteString import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.delay +import kotlin.random.Random private val defaultLoRaConfig = ConfigKt.loRaConfig { usePreset = true @@ -156,6 +157,7 @@ class MockInterface @AssistedInject constructor( private fun sendConfigResponse(configId: Int) { debug("Sending mock config response") + @Suppress("MagicNumber") /// Generate a fake node info entry fun makeNodeInfo(numIn: Int, lat: Double, lon: Double) = MeshProtos.FromRadio.newBuilder().apply { @@ -172,6 +174,7 @@ class MockInterface @AssistedInject constructor( longitudeI = Position.degI(lon) altitude = 35 time = (System.currentTimeMillis() / 1000).toInt() + precisionBits = Random.nextInt(10, 19) }.build() }.build() } diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/MapFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/map/MapFragment.kt index 5bd15eedc..359550a5b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/map/MapFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/map/MapFragment.kt @@ -1,3 +1,5 @@ +@file:Suppress("MagicNumber") + package com.geeksville.mesh.ui.map import android.content.Context @@ -30,6 +32,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -53,8 +56,8 @@ import com.geeksville.mesh.ui.ScreenFragment import com.geeksville.mesh.ui.components.IconButton import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.util.SqlTileWriterExt -import com.geeksville.mesh.util.requiredZoomLevel import com.geeksville.mesh.util.formatAgo +import com.geeksville.mesh.util.requiredZoomLevel import com.geeksville.mesh.util.zoomIn import com.geeksville.mesh.waypoint import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -103,7 +106,32 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging { } } } +} +private enum class PositionPrecision(val value: Int, val precisionMeters: Double) { + TWO(2, 5976446.981252), + THREE(3, 2988223.4850600003), + FOUR(4, 1494111.7369640006), + FIVE(5, 747055.8629159998), + SIX(6, 373527.9258920002), + SEVEN(7, 186763.95738000044), + EIGHT(8, 93381.97312400135), + NINE(9, 46690.98099600022), + TEN(10, 23345.48493200123), + ELEVEN(11, 11672.736900000944), + TWELVE(12, 5836.362884000802), + THIRTEEN(13, 2918.1758760007315), + FOURTEEN(14, 1459.0823719999053), + FIFTEEN(15, 729.5356200010741), + SIXTEEN(16, 364.7622440000765), + SEVENTEEN(17, 182.37555600115968), + EIGHTEEN(18, 91.1822120001193), + NINETEEN(19, 45.58554000039009), + TWENTY(20, 22.787204001316468), + TWENTY_ONE(21, 11.388036000988677), + TWENTY_TWO(22, 5.688452000824781), + TWENTY_THREE(23, 2.8386600007428338), + TWENTY_FOUR(24, 1.413763999910884), } @Composable @@ -120,7 +148,6 @@ private fun MapView.UpdateMarkers( fun MapView( model: UIViewModel = viewModel(), ) { - // UI Elements var cacheEstimate by remember { mutableStateOf("") } @@ -131,6 +158,7 @@ fun MapView( var zoomLevelMin = 0.0 var zoomLevelMax = 0.0 + // Map Elements var downloadRegionBoundingBox: BoundingBox? by remember { mutableStateOf(null) } var myLocationOverlay: MyLocationNewOverlay? by remember { mutableStateOf(null) } @@ -144,6 +172,7 @@ fun MapView( val hasGps = context.hasGps() val map = rememberMapViewWithLifecycle(context) + val primaryColor = ContextCompat.getColor(context, R.color.colorPrimary) fun MapView.toggleMyLocation() { if (context.gpsDisabled()) { @@ -218,6 +247,23 @@ fun MapView( position = GeoPoint(p.latitude, p.longitude) icon = markerIcon + PositionPrecision.entries.find { it.value == p.precisionBits }?.let { precision -> + if (precision in PositionPrecision.TEN..PositionPrecision.NINETEEN) { + if ((precision.precisionMeters) > 0) { + val circle = Polygon.pointsAsCircle( + position, + precision.precisionMeters + ) + val polygon = Polygon(this@onNodesChanged) + polygon.points = circle + polygon.fillPaint.color = primaryColor + polygon.fillPaint.alpha = 64 + polygon.outlinePaint.color = primaryColor + this@onNodesChanged.overlays.add(polygon) + } + } + } + setOnLongClickListener { performHapticFeedback() model.focusUserNode(node) @@ -263,7 +309,8 @@ fun MapView( } fun getUsername(id: String?) = if (id == DataPacket.ID_LOCAL) context.getString(R.string.you) - else model.nodeDB.nodes.value[id]?.user?.longName ?: context.getString(R.string.unknown_username) + else model.nodeDB.nodes.value[id]?.user?.longName + ?: context.getString(R.string.unknown_username) fun MapView.onWaypointChanged(waypoints: Collection): List { return waypoints.mapNotNull { waypoint -> @@ -528,7 +575,8 @@ fun MapView( append("mainFile.sqlite") // TODO: Accept filename input param from user } val writer = SqliteArchiveTileWriter(outputName) - val cacheManager = CacheManager(map, writer) // Make sure cacheManager has latest from map + val cacheManager = + CacheManager(map, writer) // Make sure cacheManager has latest from map //this triggers the download downloadRegion( cacheManager,