From b053f1afda3c912b2e191958030a6ae4d7bafcb3 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 9 Nov 2024 05:34:14 -0300 Subject: [PATCH] refactor: extract map defaults to `MapViewWithLifecycle` --- .../geeksville/mesh/ui/components/NodeMap.kt | 49 ++------------- .../com/geeksville/mesh/ui/map/MapFragment.kt | 59 ++++-------------- .../mesh/ui/map/MapViewWithLifecycle.kt | 60 ++++++++++++++++++- .../geeksville/mesh/util/MapViewExtensions.kt | 19 ------ 4 files changed, 75 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/NodeMap.kt b/app/src/main/java/com/geeksville/mesh/ui/components/NodeMap.kt index e179038b9..426eccd84 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/NodeMap.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/NodeMap.kt @@ -3,30 +3,20 @@ 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.runtime.remember 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.addPositionMarkers 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 @@ -34,44 +24,15 @@ private const val DegD = 1e-7 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 - } - } + val cameraView = remember { BoundingBox.fromGeoPoints(geoPoints) } + val mapView = rememberMapViewWithLifecycle(cameraView) 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) - } - }, + factory = { mapView }, update = { map -> map.overlays.clear() map.addCopyright() 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 84a451460..6e7277bb0 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 @@ -38,7 +38,6 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.fragment.app.activityViewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.geeksville.mesh.BuildConfig import com.geeksville.mesh.DataPacket import com.geeksville.mesh.MeshProtos.Waypoint import com.geeksville.mesh.R @@ -54,11 +53,14 @@ import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.map.CustomTileSource import com.geeksville.mesh.model.map.MarkerWithLabel +import com.geeksville.mesh.model.map.clustering.RadiusMarkerClusterer import com.geeksville.mesh.ui.ScreenFragment import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.util.SqlTileWriterExt +import com.geeksville.mesh.util.addCopyright +import com.geeksville.mesh.util.addScaleBarOverlay +import com.geeksville.mesh.util.createLatLongGrid 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 @@ -76,18 +78,12 @@ import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase import org.osmdroid.tileprovider.tilesource.TileSourcePolicyException import org.osmdroid.util.BoundingBox import org.osmdroid.util.GeoPoint -import org.osmdroid.views.CustomZoomButtonsController import org.osmdroid.views.MapView import org.osmdroid.views.overlay.MapEventsOverlay import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Polygon import org.osmdroid.views.overlay.infowindow.InfoWindow import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay -import com.geeksville.mesh.model.map.clustering.RadiusMarkerClusterer -import com.geeksville.mesh.util.addCopyright -import com.geeksville.mesh.util.addMapEventListener -import com.geeksville.mesh.util.addScaleBarOverlay -import com.geeksville.mesh.util.createLatLongGrid import java.io.File import java.text.DateFormat @@ -211,8 +207,6 @@ private fun Context.purgeTileSource(onResult: (String) -> Unit) { builder.show() } -private const val MaxZoomLevel = 20.0 - @Composable fun MapView( model: UIViewModel = viewModel(), @@ -240,8 +234,11 @@ fun MapView( val hasGps = remember { context.hasGps() } - val map = rememberMapViewWithLifecycle(context) - val state by model.mapState.collectAsStateWithLifecycle() + val cameraView = remember { + val geoPoints = model.nodesWithPosition.map { GeoPoint(it.latitude, it.longitude) } + BoundingBox.fromGeoPoints(geoPoints) + } + val map = rememberMapViewWithLifecycle(cameraView) val nodeClusterer = remember { RadiusMarkerClusterer(context) } @@ -370,11 +367,11 @@ fun MapView( } fun MapView.onWaypointChanged(waypoints: Collection): List { + val dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) return waypoints.mapNotNull { waypoint -> val pt = waypoint.data.waypoint ?: return@mapNotNull null val lock = if (pt.lockedTo != 0) "\uD83D\uDD12" else "" - val time = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) - .format(waypoint.received_time) + val time = dateFormat.format(waypoint.received_time) val label = pt.name + " " + formatAgo((waypoint.received_time / 1000).toInt()) val emoji = String(Character.toChars(if (pt.icon == 0) 128205 else pt.icon)) MarkerWithLabel(this, label, emoji).apply { @@ -454,20 +451,6 @@ fun MapView( UpdateMarkers(onNodesChanged(nodes), onWaypointChanged(waypoints.values), nodeClusterer) } - fun MapView.zoomToNodes() { - 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 finalZoomLevel = minOf(box.requiredZoomLevel(), maxZoomLevel) - controller.setCenter(center) - controller.setZoom(finalZoomLevel) - } else { - controller.setCenter(state.center) - controller.setZoom(state.zoom) - } - } - fun loadOnlineTileSourceBase(): ITileSource { val id = mPrefs.getInt(mapStyleId, 0) debug("mapStyleId from prefs: $id") @@ -603,29 +586,9 @@ fun MapView( AndroidView( factory = { map.apply { - // Required to get online tiles - Configuration.getInstance().userAgentValue = BuildConfig.APPLICATION_ID setTileSource(loadOnlineTileSourceBase()) setDestroyMode(false) // keeps map instance alive when in the background - isVerticalMapRepetitionEnabled = false // disables map repetition - setMultiTouchControls(true) - setScrollableAreaLimitLatitude( // bounds scrollable map - overlayManager.tilesOverlay.bounds.actualNorth, - overlayManager.tilesOverlay.bounds.actualSouth, - 0 - ) - // scales the map tiles to the display density of the screen - isTilesScaledToDpi = true - // sets the minimum zoom level (the furthest out you can zoom) - minZoomLevel = 1.5 - maxZoomLevel = MaxZoomLevel - // Disables default +/- button for zooming - zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) addMapListener(boxOverlayListener) - addMapEventListener { - model.updateMapCenterAndZoom(projection.currentCenter, zoomLevelDouble) - } - zoomToNodes() } }, modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt b/app/src/main/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt index 2d86562be..7605f7a8f 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt @@ -5,11 +5,24 @@ import android.content.Context import android.os.PowerManager import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableDoubleStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import com.geeksville.mesh.BuildConfig import com.geeksville.mesh.android.BuildUtils.errormsg +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 import org.osmdroid.views.MapView @SuppressLint("WakelockTimeout") @@ -31,11 +44,51 @@ private fun PowerManager.WakeLock.safeRelease() { } } +private const val MinZoomLevel = 1.5 +private const val MaxZoomLevel = 20.0 + @Composable -internal fun rememberMapViewWithLifecycle(context: Context): MapView { +internal fun rememberMapViewWithLifecycle(box: BoundingBox): MapView { + val zoom = box.requiredZoomLevel() + val center = GeoPoint(box.centerLatitude, box.centerLongitude) + return rememberMapViewWithLifecycle(zoom, center) +} + +@Composable +internal fun rememberMapViewWithLifecycle( + zoomLevel: Double = MinZoomLevel, + mapCenter: GeoPoint = GeoPoint(0.0, 0.0), +): MapView { + var savedZoom by rememberSaveable { mutableDoubleStateOf(zoomLevel) } + 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) } + )) { mutableStateOf(mapCenter) } + + val context = LocalContext.current val mapView = remember { MapView(context).apply { clipToOutline = true + + // Required to get online tiles + Configuration.getInstance().userAgentValue = BuildConfig.APPLICATION_ID + isVerticalMapRepetitionEnabled = false // disables map repetition + setMultiTouchControls(true) + setScrollableAreaLimitLatitude( // bounds scrollable map + overlayManager.tilesOverlay.bounds.actualNorth, + overlayManager.tilesOverlay.bounds.actualSouth, + 0 + ) + // scales the map tiles to the display density of the screen + isTilesScaledToDpi = true + // sets the minimum zoom level (the furthest out you can zoom) + minZoomLevel = MinZoomLevel + maxZoomLevel = MaxZoomLevel + // Disables default +/- button for zooming + zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) + + controller.setZoom(savedZoom) + controller.setCenter(savedCenter) } } val lifecycle = LocalLifecycleOwner.current.lifecycle @@ -60,6 +113,11 @@ internal fun rememberMapViewWithLifecycle(context: Context): MapView { mapView.onResume() } + Lifecycle.Event.ON_STOP -> { + savedCenter = mapView.projection.currentCenter + savedZoom = mapView.zoomLevelDouble + } + else -> {} } } diff --git a/app/src/main/java/com/geeksville/mesh/util/MapViewExtensions.kt b/app/src/main/java/com/geeksville/mesh/util/MapViewExtensions.kt index 7a88eb224..15bf8b250 100644 --- a/app/src/main/java/com/geeksville/mesh/util/MapViewExtensions.kt +++ b/app/src/main/java/com/geeksville/mesh/util/MapViewExtensions.kt @@ -10,10 +10,6 @@ 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 @@ -75,21 +71,6 @@ fun MapView.addScaleBarOverlay(density: Density) { } } -private const val INACTIVITY_DELAY_MILLIS = 500L -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)) -} - fun MapView.addPolyline( density: Density, geoPoints: List,