mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: extract map defaults to MapViewWithLifecycle
This commit is contained in:
parent
227c65f191
commit
b053f1afda
4 changed files with 75 additions and 112 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue