refactor: extract map defaults to MapViewWithLifecycle

This commit is contained in:
andrekir 2024-11-09 05:34:14 -03:00
parent 227c65f191
commit b053f1afda
4 changed files with 75 additions and 112 deletions

View file

@ -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()

View file

@ -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<Packet>): List<MarkerWithLabel> {
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(),

View file

@ -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 -> {}
}
}

View file

@ -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<GeoPoint>,