Skip to content

Commit 806d01c

Browse files
committed
fix: Restore the last viewed map center and zoom
1 parent 281cf51 commit 806d01c

4 files changed

Lines changed: 152 additions & 99 deletions

File tree

‎app/src/main/java/org/nitri/opentopo/BaseMainActivity.kt‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,11 +352,8 @@ open class BaseMainActivity : AppCompatActivity(), MapFragment.OnFragmentInterac
352352
) {
353353
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
354354
if (requestCode == REQUEST_LOCATION_PERMISSION) {
355-
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
356-
addMapFragment()
357-
} else {
358-
finish()
359-
}
355+
// Location is optional: the map can restore its viewport or show the fallback.
356+
addMapFragment()
360357
}
361358
}
362359

‎app/src/main/java/org/nitri/opentopo/MapFragment.kt‎

Lines changed: 77 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
9292
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
9393
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
9494
import java.io.File
95-
import kotlin.math.min
9695

9796
class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListener,
9897
GestureCallback, ClickableCompassOverlay.OnCompassClickListener {
@@ -171,6 +170,9 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
171170
private var gpxViewModel: GpxViewModel? = null
172171
private var gestureOverlay: GestureOverlay? = null
173172

173+
private var firstFixZoom = DEFAULT_ZOOM
174+
private var awaitingFirstFix = false
175+
private var explicitViewport = false
174176
private var mapViewInitialized = false
175177

176178
private val markerViewModel: MarkerViewModel by viewModels()
@@ -388,7 +390,12 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
388390

389391
rotationGestureOverlay = RotationGestureOverlay(mapView)
390392
rotationGestureOverlay?.isEnabled = true
391-
gestureOverlay = GestureOverlay(this)
393+
gestureOverlay = GestureOverlay(object : GestureCallback {
394+
override fun onUserMapInteraction() {
395+
awaitingFirstFix = false
396+
this@MapFragment.onUserMapInteraction()
397+
}
398+
})
392399
mapView.overlays.add(gestureOverlay)
393400
mapView.maxZoomLevel = maxZoomLevel
394401
mapView.isTilesScaledToDpi = true
@@ -398,6 +405,13 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
398405
mapView.overlays.add(locationOverlay)
399406
mapView.overlays.add(compassOverlay)
400407
mapView.overlays.add(scaleBarOverlay)
408+
mapView.addMapListener(object : MapListener {
409+
override fun onScroll(event: ScrollEvent) = false
410+
override fun onZoom(event: ZoomEvent): Boolean {
411+
if (event.zoomLevel != firstFixZoom) awaitingFirstFix = false
412+
return false
413+
}
414+
})
401415
mapView.addMapListener(DelayedMapListener(dragListener))
402416
copyrightView = view?.findViewById(R.id.copyrightView)
403417
setBaseMap()
@@ -449,21 +463,6 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
449463
}
450464

451465
private fun initializeMapState(savedInstanceState: Bundle?) {
452-
listener?.setGpx()
453-
listener?.setKml()
454-
455-
val arguments = arguments
456-
var mapCenterSet = false
457-
// Move to received geo intent coordinates
458-
arguments?.let {
459-
val lat = it.getDouble(PARAM_LATITUDE, Double.MIN_VALUE)
460-
val lon = it.getDouble(PARAM_LONGITUDE, Double.MIN_VALUE)
461-
if (lat != Double.MIN_VALUE && lon != Double.MIN_VALUE) {
462-
mapCenterSet = true
463-
animateToLatLon(lat, lon)
464-
}
465-
}
466-
467466
val hostActivity = activity
468467
val hasFineLocationPermission =
469468
hostActivity?.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
@@ -480,53 +479,41 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
480479
}
481480
}
482481
}
483-
savedInstanceState?.let {
484-
if (it.containsKey(STATE_LATITUDE) && savedInstanceState.containsKey(STATE_LONGITUDE)
485-
) {
486-
mapCenterState = GeoPoint(
487-
it.getDouble(STATE_LATITUDE, 0.0),
488-
it.getDouble(STATE_LONGITUDE, 0.0)
489-
)
490-
Log.d(
491-
TAG,
492-
String.format(
493-
"Restoring center state: %f, %f",
494-
mapCenterState?.latitude,
495-
mapCenterState?.longitude
496-
)
497-
)
498-
zoomState = it.getDouble(STATE_ZOOM, DEFAULT_ZOOM)
499-
} else {
500-
Log.d(TAG, "No center state delivered")
501-
}
502-
}
503-
if (mapCenterState == null) {
504-
Log.d(TAG, "No saved center state")
505-
val prefLat = sharedPreferences.getFloat(PREF_LATITUDE, 0f)
506-
val prefLon = sharedPreferences.getFloat(PREF_LONGITUDE, 0f)
507-
if (prefLat > 0 && prefLon > 0) {
508-
mapCenterState = GeoPoint(prefLat.toDouble(), prefLon.toDouble())
509-
Log.d(
510-
TAG,
511-
String.format("Restoring center state from prefs: %f, %f", prefLat, prefLon)
512-
)
513-
}
514-
}
515-
mapCenterState?.let {
516-
mapView.controller.setCenter(it)
517-
mapCenterSet = true
518-
} ?: run {
519-
locationViewModel?.currentLocation?.value?.let { location ->
520-
mapView.controller.setCenter(GeoPoint(location.latitude, location.longitude))
521-
mapCenterSet = true
482+
val restored = savedInstanceState?.let {
483+
MapViewport.restore(
484+
if (it.containsKey(STATE_LATITUDE)) it.getDouble(STATE_LATITUDE) else null,
485+
if (it.containsKey(STATE_LONGITUDE)) it.getDouble(STATE_LONGITUDE) else null,
486+
it.getDouble(STATE_ZOOM, DEFAULT_ZOOM)
487+
)
488+
} ?: mapCenterState?.let { MapViewport(it.latitude, it.longitude, zoomState) }
489+
val persisted = MapViewport.fromPreferences(sharedPreferences.all)
490+
val known = locationViewModel?.currentLocation?.value?.let {
491+
MapViewport.restore(it.latitude, it.longitude, DEFAULT_ZOOM)
492+
}
493+
val viewport = MapViewport.select(restored, persisted, known)
494+
firstFixZoom = viewport.clampedZoom(mapView.minZoomLevel, mapView.maxZoomLevel)
495+
mapView.controller.setZoom(firstFixZoom)
496+
mapView.controller.setCenter(GeoPoint(viewport.latitude, viewport.longitude))
497+
awaitingFirstFix = restored == null && persisted == null && known == null
498+
explicitViewport = false
499+
500+
// Consume navigation arguments once; recreation should restore the viewed viewport.
501+
arguments?.let {
502+
val destination = MapViewport.restore(
503+
if (it.containsKey(PARAM_LATITUDE)) it.getDouble(PARAM_LATITUDE) else null,
504+
if (it.containsKey(PARAM_LONGITUDE)) it.getDouble(PARAM_LONGITUDE) else null,
505+
DEFAULT_ZOOM
506+
)
507+
if (destination != null && savedInstanceState == null && mapCenterState == null) {
508+
animateToLatLon(destination.latitude, destination.longitude)
522509
}
510+
it.remove(PARAM_LATITUDE)
511+
it.remove(PARAM_LONGITUDE)
523512
}
524-
if (!mapCenterSet) {
525-
centerOnFirstFix()
526-
}
527-
zoomState = min(zoomState, maxZoomLevel)
528-
mapView.controller.setZoom(zoomState)
529-
if (sharedPreferences.getBoolean(PREF_FOLLOW, false)) enableFollow()
513+
listener?.setGpx()
514+
listener?.setKml()
515+
if (awaitingFirstFix) centerOnFirstFix()
516+
if (!explicitViewport && sharedPreferences.getBoolean(PREF_FOLLOW, false)) enableFollow()
530517

531518
setKeepScreenOn(sharedPreferences.getBoolean(PREF_KEEP_SCREEN_ON, false))
532519

@@ -659,21 +646,22 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
659646
}
660647

661648
private fun centerOnFirstFix() {
662-
locationOverlay?.runOnFirstFix {
663-
val location = locationOverlay?.myLocation
664-
location?.let {
665-
try {
666-
activity?.runOnUiThread {
667-
mapView.controller.animateTo(it)
668-
}
669-
} catch (e: IllegalStateException) {
670-
e.printStackTrace()
649+
val startupMap = mapView
650+
val startupOverlay = locationOverlay
651+
startupOverlay?.runOnFirstFix {
652+
activity?.runOnUiThread {
653+
if (view == null || mapView !== startupMap) return@runOnUiThread
654+
if (awaitingFirstFix) {
655+
startupOverlay.myLocation?.let { startupMap.controller.animateTo(it) }
671656
}
657+
awaitingFirstFix = false
672658
}
673659
}
674660
}
675661

676662
private fun animateToLatLon(lat: Double, lon: Double) {
663+
awaitingFirstFix = false
664+
explicitViewport = true
677665
mapHandler.postDelayed({
678666
disableFollow()
679667
mapView.controller.animateTo(GeoPoint(lat, lon))
@@ -792,6 +780,7 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
792780
}
793781

794782
private fun disableFollow() {
783+
awaitingFirstFix = false
795784
followEnabled = false
796785
activity?.let { (it as AppCompatActivity).supportInvalidateOptionsMenu() }
797786
locationOverlay?.disableFollowLocation()
@@ -800,17 +789,14 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
800789
}
801790

802791
private fun saveMapCenterPrefs() {
803-
mapCenterState?.let {
804-
sharedPreferences.edit { putFloat(PREF_LATITUDE, it.latitude.toFloat()) }
805-
sharedPreferences.edit { putFloat(PREF_LONGITUDE, it.longitude.toFloat()) }
806-
Log.d(
807-
TAG,
808-
String.format(
809-
"Saving center prefs: %f, %f",
810-
it.latitude,
811-
it.longitude
812-
)
813-
)
792+
if (!mapViewInitialized) return
793+
val center = mapView.mapCenter
794+
mapCenterState = GeoPoint(center.latitude, center.longitude)
795+
zoomState = mapView.zoomLevelDouble
796+
sharedPreferences.edit {
797+
putFloat(PREF_LATITUDE, center.latitude.toFloat())
798+
putFloat(PREF_LONGITUDE, center.longitude.toFloat())
799+
putFloat(PREF_ZOOM, zoomState.toFloat())
814800
}
815801
}
816802

@@ -853,15 +839,6 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
853839
compassOverlay?.enableCompass()
854840
scaleBarOverlay?.enableScaleBar()
855841
mapView.invalidate()
856-
mapCenterState?.let {
857-
mapView.controller.setCenter(mapCenterState)
858-
mapCenterState = null // We're done with the old state
859-
} ?: run {
860-
locationViewModel?.currentLocation?.value?.let { location ->
861-
val geoPoint = GeoPoint(location.latitude, location.longitude)
862-
mapView.controller.setCenter(geoPoint)
863-
}
864-
}
865842
locationManager?.let {
866843
try {
867844
it.requestLocationUpdates(
@@ -887,8 +864,9 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
887864
}
888865

889866
override fun onPause() {
867+
awaitingFirstFix = false
890868
if (::mapView.isInitialized) {
891-
mapCenterState = mapView.mapCenter as GeoPoint
869+
saveMapCenterPrefs()
892870
}
893871
try {
894872
locationManager?.removeUpdates(this)
@@ -922,7 +900,8 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
922900
)
923901
)
924902
}
925-
outState.putDouble(STATE_ZOOM, mapView.zoomLevelDouble)
903+
zoomState = mapView.zoomLevelDouble
904+
outState.putDouble(STATE_ZOOM, zoomState)
926905
}
927906
super.onSaveInstanceState(outState)
928907
}
@@ -989,6 +968,8 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
989968
}
990969

991970
private fun zoomToBounds(box: BoundingBox) {
971+
awaitingFirstFix = false
972+
explicitViewport = true
992973
mapView.post {
993974
if (mapView.height > 0) {
994975
mapView.zoomToBoundingBox(box, true, 64)
@@ -1107,6 +1088,7 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
11071088
return true
11081089
}
11091090
R.id.action_location -> {
1091+
awaitingFirstFix = false
11101092
locationViewModel?.currentLocation?.value?.let { location ->
11111093
mapView.controller.animateTo(GeoPoint(location))
11121094
}
@@ -1206,7 +1188,6 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
12061188
return false
12071189
}
12081190
R.id.action_settings -> {
1209-
mapCenterState = mapView.mapCenter as GeoPoint
12101191
saveMapCenterPrefs()
12111192
listener?.openSettings()
12121193
return true
@@ -1360,6 +1341,7 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
13601341
}
13611342

13621343
override fun onDestroyView() {
1344+
awaitingFirstFix = false
13631345
cacheRevisionJob?.cancel()
13641346
mapView.tileProvider.detach()
13651347
super.onDestroyView()
@@ -1558,6 +1540,7 @@ class MapFragment : Fragment(), LocationListener, PopupMenu.OnMenuItemClickListe
15581540
private const val PREF_OVERLAY = "overlay"
15591541
private const val PREF_LATITUDE = "latitude"
15601542
private const val PREF_LONGITUDE = "longitude"
1543+
private const val PREF_ZOOM = "zoom"
15611544
private const val PREF_FOLLOW = "follow"
15621545
private const val BASE_MAP_OTM = 1
15631546
private const val BASE_MAP_OSM = 2
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.nitri.opentopo
2+
3+
/** Viewport restoration policy, independent of Android lifecycle and location permissions. */
4+
internal data class MapViewport(val latitude: Double, val longitude: Double, val zoom: Double) {
5+
fun clampedZoom(minimum: Double, maximum: Double): Double =
6+
(zoom.takeIf { it.isFinite() } ?: DEFAULT_ZOOM).coerceIn(minimum, maximum)
7+
8+
companion object {
9+
const val DEFAULT_ZOOM = 15.0
10+
val FALLBACK = MapViewport(51.78962, 6.14120, DEFAULT_ZOOM)
11+
12+
fun restore(latitude: Double?, longitude: Double?, zoom: Double): MapViewport? {
13+
if (latitude == null || longitude == null ||
14+
!latitude.isFinite() || !longitude.isFinite() ||
15+
latitude !in -90.0..90.0 || longitude !in -180.0..180.0) return null
16+
return MapViewport(latitude, longitude, zoom)
17+
}
18+
19+
fun fromPreferences(values: Map<String, *>): MapViewport? = restore(
20+
(values["latitude"] as? Float)?.toDouble(),
21+
(values["longitude"] as? Float)?.toDouble(),
22+
(values["zoom"] as? Float)?.toDouble() ?: DEFAULT_ZOOM
23+
)
24+
25+
fun select(restored: MapViewport?, persisted: MapViewport?, known: MapViewport?): MapViewport =
26+
restored ?: persisted ?: known ?: FALLBACK
27+
}
28+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.nitri.opentopo
2+
3+
import org.junit.Assert.*
4+
import org.junit.Test
5+
6+
class MapViewportTest {
7+
@Test fun `legacy float preferences accept all hemispheres and zero`() {
8+
for ((lat, lon) in listOf(-33f to -70f, 0f to 6f, 51f to 0f, 0f to 0f)) {
9+
val viewport = MapViewport.fromPreferences(mapOf("latitude" to lat, "longitude" to lon))!!
10+
assertEquals(lat.toDouble(), viewport.latitude, 0.0)
11+
assertEquals(lon.toDouble(), viewport.longitude, 0.0)
12+
assertEquals(15.0, viewport.zoom, 0.0)
13+
}
14+
}
15+
16+
@Test fun `missing and invalid coordinates are rejected`() {
17+
assertNull(MapViewport.fromPreferences(emptyMap<String, Float>()))
18+
assertNull(MapViewport.fromPreferences(mapOf("latitude" to 0f)))
19+
assertNull(MapViewport.fromPreferences(mapOf("longitude" to 0f)))
20+
for ((lat, lon) in listOf(Float.NaN to 0f, 0f to Float.POSITIVE_INFINITY,
21+
-91f to 0f, 91f to 0f, 0f to -181f, 0f to 181f)) {
22+
assertNull(MapViewport.fromPreferences(mapOf("latitude" to lat, "longitude" to lon)))
23+
}
24+
}
25+
26+
@Test fun `persisted zoom is restored and constrained to supported limits`() {
27+
val viewport = MapViewport.fromPreferences(mapOf("latitude" to 51f, "longitude" to 6f, "zoom" to 16.5f))!!
28+
assertEquals(16.5, viewport.clampedZoom(1.0, 17.0), 0.0)
29+
assertEquals(14.0, viewport.clampedZoom(1.0, 14.0), 0.0)
30+
assertEquals(1.0, viewport.copy(zoom = -2.0).clampedZoom(1.0, 17.0), 0.0)
31+
for (zoom in listOf(Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY)) {
32+
assertEquals(15.0, viewport.copy(zoom = zoom).clampedZoom(1.0, 17.0), 0.0)
33+
}
34+
}
35+
36+
@Test fun `selection prefers restored viewport then preferences then location then fallback`() {
37+
val state = MapViewport(1.0, 2.0, 10.0)
38+
val prefs = MapViewport(3.0, 4.0, 11.0)
39+
val location = MapViewport(5.0, 6.0, 15.0)
40+
assertEquals(state, MapViewport.select(state, prefs, location))
41+
assertEquals(prefs, MapViewport.select(null, prefs, location))
42+
assertEquals(location, MapViewport.select(null, null, location))
43+
assertEquals(MapViewport(51.78962, 6.14120, 15.0), MapViewport.select(null, null, null))
44+
}
45+
}

0 commit comments

Comments
 (0)