From 3bd4132089b6edb117513fae77d38a36da82ee2e Mon Sep 17 00:00:00 2001 From: andrekir Date: Mon, 30 Sep 2024 19:13:56 -0300 Subject: [PATCH] feat: preserve map center and zoom state in ViewModel closes #1150 --- .../java/com/geeksville/mesh/model/UIState.kt | 21 +++++++-- .../com/geeksville/mesh/ui/map/MapFragment.kt | 44 ++++++++++++------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 15d860e7..c0573460 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -24,7 +24,6 @@ import com.geeksville.mesh.database.QuickChatActionRepository import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.QuickChatAction -import com.geeksville.mesh.database.entity.toNodeInfo import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.geeksville.mesh.service.MeshService @@ -43,8 +42,10 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.osmdroid.util.GeoPoint import java.io.BufferedWriter import java.io.FileNotFoundException import java.io.FileWriter @@ -119,6 +120,15 @@ data class NodesUiState( } } +data class MapState( + val center: GeoPoint? = null, + val zoom: Double = 0.0, +) { + companion object { + val Empty = MapState() + } +} + data class Contact( val contactKey: String, val shortName: String, @@ -231,8 +241,13 @@ class UIViewModel @Inject constructor( val myNodeInfo: StateFlow get() = nodeDB.myNodeInfo val ourNodeInfo: StateFlow get() = nodeDB.ourNodeInfo - // FIXME only used in MapFragment - val initialNodes get() = nodeDB.nodeDBbyNum.value.values.map { it.toNodeInfo() } + val nodesWithPosition get() = nodeDB.nodeDBbyNum.value.values.filter { it.validPosition != null } + + private val _mapState = MutableStateFlow(MapState.Empty) + val mapState: StateFlow get() = _mapState + + fun updateMapCenterAndZoom(center: GeoPoint, zoom: Double) = + _mapState.update { it.copy(center = center, zoom = zoom) } fun getUser(userId: String?) = nodeDB.getUser(userId) ?: user { id = userId.orEmpty() 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 5bfe4365..ab8841bc 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 @@ -64,6 +64,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.mapLatest import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable import org.osmdroid.config.Configuration +import org.osmdroid.events.DelayedMapListener import org.osmdroid.events.MapEventsReceiver import org.osmdroid.events.MapListener import org.osmdroid.events.ScrollEvent @@ -238,6 +239,21 @@ private fun Context.purgeTileSource(onResult: (String) -> Unit) { builder.show() } +private const val INACTIVITY_DELAY_MILLIS = 500L +private fun MapView.addMapEventListener(onEvent: () -> Unit) { + addMapListener(DelayedMapListener(object : MapListener { + override fun onScroll(event: ScrollEvent): Boolean { + onEvent() + return true + } + + override fun onZoom(event: ZoomEvent): Boolean { + onEvent() + return true + } + }, INACTIVITY_DELAY_MILLIS)) +} + @Composable fun MapView( model: UIViewModel = viewModel(), @@ -265,6 +281,7 @@ fun MapView( val hasGps = remember { context.hasGps() } val map = rememberMapViewWithLifecycle(context) + val state by model.mapState.collectAsStateWithLifecycle() fun MapView.toggleMyLocation() { if (context.gpsDisabled()) { @@ -473,15 +490,17 @@ fun MapView( } fun MapView.zoomToNodes() { - val nodeMarkers = onNodesChanged(model.initialNodes) - if (nodeMarkers.isNotEmpty()) { - val box = BoundingBox.fromGeoPoints(nodeMarkers.map { it.position }) + if (state.center == null) { + val geoPoints = model.nodesWithPosition.map { GeoPoint(it.latitude, it.longitude) } + val box = BoundingBox.fromGeoPoints(geoPoints) val center = GeoPoint(box.centerLatitude, box.centerLongitude) - val maximumZoomLevel = tileProvider.tileSource.maximumZoomLevel.toDouble() - val finalZoomLevel = minOf(box.requiredZoomLevel() * 0.8, maximumZoomLevel) + val finalZoomLevel = minOf(box.requiredZoomLevel() * 0.8, maxZoomLevel) controller.setCenter(center) controller.setZoom(finalZoomLevel) - } else controller.zoomIn() + } else { + controller.setCenter(state.center) + controller.setZoom(state.zoom) + } } fun loadOnlineTileSourceBase(): ITileSource { @@ -624,16 +643,9 @@ fun MapView( minZoomLevel = 1.5 // Disables default +/- button for zooming zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) - addMapListener(object : MapListener { - override fun onScroll(event: ScrollEvent): Boolean { - if (downloadRegionBoundingBox != null) generateBoxOverlay() - return true - } - - override fun onZoom(event: ZoomEvent): Boolean { - return false - } - }) + addMapEventListener { + model.updateMapCenterAndZoom(map.projection.currentCenter, map.zoomLevelDouble) + } zoomToNodes() } },