diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 1619bbe63ad..1209b38f5bb 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -81,7 +81,7 @@ dependencies { debugImplementation("androidx.compose.ui:ui-tooling:1.10.0") // location - implementation("org.maplibre.compose:location:0.15.0") + implementation("org.maplibre.compose:location:0.16.0") // Dependency Injection implementation("io.insert-koin:koin-android:4.2.2") diff --git a/androidApp/src/main/kotlin/de/westnordost/streetcomplete/screens/main/MainActivity.kt b/androidApp/src/main/kotlin/de/westnordost/streetcomplete/screens/main/MainActivity.kt index 71465e60fbf..56fea39732c 100644 --- a/androidApp/src/main/kotlin/de/westnordost/streetcomplete/screens/main/MainActivity.kt +++ b/androidApp/src/main/kotlin/de/westnordost/streetcomplete/screens/main/MainActivity.kt @@ -78,6 +78,7 @@ import de.westnordost.streetcomplete.util.ktx.observe import de.westnordost.streetcomplete.util.ktx.toLatLon import de.westnordost.streetcomplete.util.ktx.toOffset import de.westnordost.streetcomplete.util.ktx.toast +import de.westnordost.streetcomplete.util.ktx.updatesWithPermissionChanges import de.westnordost.streetcomplete.util.math.area import de.westnordost.streetcomplete.util.math.enclosingBoundingBox import de.westnordost.streetcomplete.util.math.enlargedBy @@ -348,15 +349,14 @@ class MainActivity : mainBottomSheetViewModel.closeBottomSheet() } } - observe(locationProvider.updates(LocationRequest())) { locationEvent -> + observe(locationProvider.updatesWithPermissionChanges(LocationRequest())) { locationEvent -> viewModel.locationState.value = when (locationEvent) { - is LocationEvent.Fix -> LocationState.UPDATING + is LocationEvent.Update -> LocationState.UPDATING is LocationEvent.Unavailable -> when (locationEvent.reason) { LocationUnavailableReason.ServicesDisabled -> LocationState.ALLOWED LocationUnavailableReason.TemporarilyUnavailable -> LocationState.SEARCHING LocationUnavailableReason.PermissionDenied -> LocationState.DENIED LocationUnavailableReason.Unsupported, - LocationUnavailableReason.Misconfigured, LocationUnavailableReason.UnexpectedFailure -> null } } @@ -490,7 +490,7 @@ class MainActivity : private fun getDisplayedPoint(): PointF? { val mapFragment = mapFragment ?: return null - val displayedPosition = mapFragment.displayedLocation?.position?.value?.toLatLon() ?: return null + val displayedPosition = mapFragment.displayedLocation?.position?.toLatLon() ?: return null return mapFragment.getPointOf(displayedPosition) } @@ -588,7 +588,7 @@ class MainActivity : viewModel.isRecordingTracks.value = false val mapFragment = mapFragment ?: return mapFragment.stopPositionTrackRecording() - val pos = mapFragment.displayedLocation?.position?.value?.toLatLon() ?: return + val pos = mapFragment.displayedLocation?.position?.toLatLon() ?: return composeNote(pos, mapFragment.recordedTracks.takeIf { it.isNotEmpty() }) } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 83c1baba945..c5ace793101 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -146,7 +146,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.9.1") // location - implementation("org.maplibre.compose:location:0.15.0") + implementation("org.maplibre.compose:location:0.16.0") // SQLite implementation("androidx.sqlite:sqlite:2.7.0") diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapFragment.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapFragment.kt index 29d761bd41e..64b0593224f 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapFragment.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/MainMapFragment.kt @@ -49,17 +49,17 @@ import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.Style import org.maplibre.android.style.layers.Property import org.maplibre.android.style.layers.PropertyFactory.visibility -import org.maplibre.compose.location.AndroidOrientationProvider -import org.maplibre.compose.location.Location +import org.maplibre.compose.location.AndroidHeadingProvider +import org.maplibre.compose.location.HeadingMeasurement +import org.maplibre.compose.location.HeadingProvider +import org.maplibre.compose.location.HeadingRequest import org.maplibre.compose.location.LocationEvent -import org.maplibre.compose.location.Orientation -import org.maplibre.compose.location.OrientationProvider -import org.maplibre.compose.location.PositionWithAccuracy +import org.maplibre.compose.location.LocationMeasurement import org.maplibre.spatialk.units.Bearing -import org.maplibre.spatialk.units.DMS import org.maplibre.spatialk.units.International +import org.maplibre.spatialk.units.extensions.degrees +import org.maplibre.spatialk.units.extensions.inDegrees import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.TimeSource /** This is the map shown in the main view. It manages a map that shows the quest pins, quest * geometry, overlays, tracks, location... */ @@ -75,7 +75,7 @@ class MainMapFragment : MapFragment() { private val surveyChecker: SurveyChecker by inject() private val prefs: Preferences by inject() - private lateinit var orientationProvider: OrientationProvider + private lateinit var headingProvider: HeadingProvider private var mapImages: MapImages? = null private var geometryMarkersMapComponent: GeometryMarkersMapComponent? = null @@ -106,7 +106,7 @@ class MainMapFragment : MapFragment() { private var zoomedYet = false /** The GPS position at which the user is displayed at */ - var displayedLocation: Location? = null + var displayedLocation: LocationMeasurement? = null private set /** The GPS trackpoints the user has walked */ @@ -161,15 +161,11 @@ class MainMapFragment : MapFragment() { override fun onAttach(context: Context) { super.onAttach(context) - orientationProvider = AndroidOrientationProvider( - context = context, - updateInterval = 33.milliseconds, - coroutineScope = lifecycleScope - ) + headingProvider = AndroidHeadingProvider(context) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - orientationProvider.orientation.collect { orientation -> - onCompassRotationChanged(orientation) + headingProvider.updates(HeadingRequest(33.milliseconds)).collect { heading -> + onCompassRotationChanged(heading) } } } @@ -178,9 +174,7 @@ class MainMapFragment : MapFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState != null) { - val position: PositionWithAccuracy? = - savedInstanceState.getString(DISPLAYED_POSITION)?.let { Json.decodeFromString(it) } - displayedLocation = position?.let { Location(it, timestamp = TimeSource.Monotonic.markNow()) } + displayedLocation = savedInstanceState.getString(DISPLAYED_LOCATION)?.let { Json.decodeFromString(it) } isRecordingTracks = savedInstanceState.getBoolean(TRACKS_IS_RECORDING) tracks = Json.decodeFromString(savedInstanceState.getString(TRACKS)!!) } @@ -278,7 +272,7 @@ class MainMapFragment : MapFragment() { onSelectedOverlayChanged() selectedOverlaySource.addListener(overlayListener) - locationMapComponent?.targetPositionWithAccuracy = displayedLocation?.position + locationMapComponent?.targetLocation = displayedLocation val positionsLists = tracks.map { track -> track.map { it.position } } tracksMapComponent?.setTracks(positionsLists, isRecordingTracks) @@ -291,7 +285,7 @@ class MainMapFragment : MapFragment() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putString(DISPLAYED_POSITION, Json.encodeToString(displayedLocation?.position)) + outState.putString(DISPLAYED_LOCATION, Json.encodeToString(displayedLocation)) // the amount of data one can put into a bundle is limited, let's cut off at 1000 points outState.putString(TRACKS, Json.encodeToString(tracks.takeLastNested(1000))) outState.putBoolean(TRACKS_IS_RECORDING, isRecordingTracks) @@ -333,27 +327,23 @@ class MainMapFragment : MapFragment() { return true } - private fun onCompassRotationChanged(orientation: Orientation?) { - val rotation = orientation - ?.orientation - ?.value - ?.clockwiseRotationTo(Bearing.North) - ?.toDouble(DMS.Degrees) - locationMapComponent?.targetRotation = rotation?.let { rotation - (map?.camera?.rotation ?: 0.0) }?.toFloat() + private fun onCompassRotationChanged(heading: HeadingMeasurement) { + val cameraBearing = Bearing.North + (map?.camera?.rotation ?: 0.0).degrees + locationMapComponent?.targetRotation = (heading.bearing - cameraBearing).inDegrees.toFloat() } fun onLocationEvent(locationEvent: LocationEvent) { when (locationEvent) { - is LocationEvent.Fix -> { - val location = locationEvent.location + is LocationEvent.Update -> { + val location = locationEvent.measurement displayedLocation = location - surveyChecker.addRecentLocation(location.toLocation()) - locationMapComponent?.targetPositionWithAccuracy = location.position + surveyChecker.addRecentLocation(locationEvent.toLocation()) + locationMapComponent?.targetLocation = location addTrackLocation(location) centerCurrentPositionIfFollowing() } is LocationEvent.Unavailable -> { - locationMapComponent?.targetPositionWithAccuracy = null + locationMapComponent?.targetLocation = null displayedLocation = null isNavigationMode = false @@ -408,24 +398,24 @@ class MainMapFragment : MapFragment() { previouslyHiddenLayers = new } - private fun addTrackLocation(location: Location) { + private fun addTrackLocation(location: LocationMeasurement) { // ignore if too imprecise - val accuracy = location.position.accuracy?.toFloat(International.Meters) + val accuracy = location.horizontalAccuracy?.toFloat(International.Meters) if (accuracy != null && accuracy > MIN_TRACK_ACCURACY) return val lastLocation = tracks.last().lastOrNull() // create new track if last position too old if (lastLocation != null && !isRecordingTracks) { - if ((displayedLocation?.timestamp?.elapsedNow()?.inWholeMilliseconds ?: 0) - lastLocation.time > MAX_TIME_BETWEEN_LOCATIONS) { + if (location.measuredAt.toEpochMilliseconds() - lastLocation.time > MAX_TIME_BETWEEN_LOCATIONS) { tracks.add(ArrayList()) tracksMapComponent?.startNewTrack(false) } } val trackpoint = Trackpoint( - position = location.position.value.toLatLon(), - time = location.timestamp.elapsedNow().inWholeMilliseconds, + position = location.position.toLatLon(), + time = location.measuredAt.toEpochMilliseconds(), accuracy = accuracy ?: 0f, - elevation = location.position.value.altitude?.toFloat() ?: 0f + elevation = location.position.altitude?.toFloat() ?: 0f ) tracks.last().add(trackpoint) @@ -512,7 +502,7 @@ class MainMapFragment : MapFragment() { } private fun centerCurrentPosition() { - val displayedPosition = displayedLocation?.position?.value?.let { it.toLatLon() } ?: return + val displayedPosition = displayedLocation?.position?.let { it.toLatLon() } ?: return updateCameraPosition(600) { if (isNavigationMode) { @@ -571,7 +561,7 @@ class MainMapFragment : MapFragment() { //endregion companion object { - private const val DISPLAYED_POSITION = "displayed_position" + private const val DISPLAYED_LOCATION = "displayed_location" private const val TRACKS = "tracks" private const val TRACKS_IS_RECORDING = "tracks_is_recording" diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/CurrentLocationMapComponent.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/CurrentLocationMapComponent.kt index 417edaf325c..e6ba0e90c9a 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/CurrentLocationMapComponent.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/map/components/CurrentLocationMapComponent.kt @@ -25,7 +25,7 @@ import org.maplibre.android.style.layers.Property import org.maplibre.android.style.layers.PropertyFactory.* import org.maplibre.android.style.layers.SymbolLayer import org.maplibre.android.style.sources.GeoJsonSource -import org.maplibre.compose.location.PositionWithAccuracy +import org.maplibre.compose.location.LocationMeasurement import org.maplibre.geojson.Feature import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.units.International @@ -38,38 +38,40 @@ class CurrentLocationMapComponent(context: Context, mapStyle: Style, private val private val locationAnimation = ValueAnimator() /** The location the GPS location dot on the map should be animated to */ - var targetPositionWithAccuracy: PositionWithAccuracy? = null + var targetLocation: LocationMeasurement? = null @UiThread set(value) { if (field == value) return field = value - val positionWithAccuracy = this.positionWithAccuracy - if (positionWithAccuracy == null || value == null) { + val location = this.location + if (location == null || value == null) { locationAnimation.cancel() - this.positionWithAccuracy = value + this.location = value update() } else { - locationAnimation.setObjectValues(positionWithAccuracy, value) + locationAnimation.setObjectValues(location, value) locationAnimation.setEvaluator(locationTypeEvaluator) locationAnimation.start() } } /** The location of the GPS location dot on the map (animated) */ - var positionWithAccuracy: PositionWithAccuracy? = null + var location: LocationMeasurement? = null private set - private val locationTypeEvaluator = object : TypeEvaluator { - override fun evaluate(fraction: Float, s: PositionWithAccuracy, e: PositionWithAccuracy): PositionWithAccuracy { - val sp = s.value - val ep = e.value - val sa = s.accuracy - val ea = e.accuracy - return PositionWithAccuracy( - value = Position( + private val locationTypeEvaluator = object : TypeEvaluator { + override fun evaluate(fraction: Float, s: LocationMeasurement, e: LocationMeasurement): LocationMeasurement { + val sp = s.position + val ep = e.position + val sa = s.horizontalAccuracy + val ea = e.horizontalAccuracy + // Preserve measuredAt from the target measurement while animating its position and accuracy. + return e.copy( + position = Position( longitude = normalizeLongitude(sp.longitude + (ep.longitude - sp.longitude) * fraction), latitude = sp.latitude + (ep.latitude - sp.latitude) * fraction, + altitude = ep.altitude, ), - accuracy = if (ea != null && sa != null) { + horizontalAccuracy = if (ea != null && sa != null) { sa + (ea - sa) * fraction.toDouble() } else { null } ) @@ -148,7 +150,7 @@ class CurrentLocationMapComponent(context: Context, mapStyle: Style, private val locationAnimation.duration = 600L locationAnimation.interpolator = AccelerateDecelerateInterpolator() locationAnimation.addUpdateListener { - positionWithAccuracy = locationAnimation.animatedValue as PositionWithAccuracy + location = locationAnimation.animatedValue as LocationMeasurement update() } @@ -195,15 +197,15 @@ class CurrentLocationMapComponent(context: Context, mapStyle: Style, private val /** Update the GPS position shown on the map */ private fun update() { - val positionWithAccuracy = this.positionWithAccuracy - if (positionWithAccuracy == null) { + val location = this.location + if (location == null) { locationSource.clear() return } - val pos = positionWithAccuracy.value + val pos = location.position val p = JsonObject() - p.addProperty("radius", positionWithAccuracy.accuracy?.toDouble(International.Meters)) + p.addProperty("radius", location.horizontalAccuracy?.toDouble(International.Meters)) rotation?.let { p.addProperty("rotation", it) } map.style?.getLayerAs("accuracy")?.setProperties( circleRadius(inMeters(get("radius"), pos.latitude)) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.android.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.android.kt index f59a1ab5bb8..93531f68f38 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.android.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.android.kt @@ -1,15 +1,16 @@ package de.westnordost.streetcomplete.ui.util import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import org.maplibre.compose.location.AndroidLocationProvider import org.maplibre.compose.location.LocationProvider import org.maplibre.compose.location.createDefaultLocationProvider @Composable public actual fun rememberDefaultLocationProvider(): LocationProvider { val context = LocalContext.current - AndroidLocationProvider(context) - return remember(context) { createDefaultLocationProvider(context) } + val provider = remember(context) { createDefaultLocationProvider(context) } + DisposableEffect(provider) { onDispose { provider.close() } } + return provider } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/location/RecentLocations.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/location/RecentLocations.kt index 9d731096114..981f855fcdd 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/location/RecentLocations.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/location/RecentLocations.kt @@ -34,8 +34,8 @@ class RecentLocations( fun add(location: Location): Unit = lock.withLock { // only add newer locations - val firstDuration = locations.firstOrNull()?.elapsedDuration ?: Duration.ZERO - if (firstDuration >= location.elapsedDuration) return@withLock + val firstDuration = locations.firstOrNull()?.elapsedDuration + if (firstDuration != null && firstDuration >= location.elapsedDuration) return@withLock // clear from deque all older than `maxAge` before inserting new while ( diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/quest/AutoSyncer.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/quest/AutoSyncer.kt index 436d814a29a..bba0ae2bc6e 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/quest/AutoSyncer.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/quest/AutoSyncer.kt @@ -19,6 +19,7 @@ import de.westnordost.streetcomplete.data.upload.UploadController import de.westnordost.streetcomplete.data.user.UserLoginSource import de.westnordost.streetcomplete.data.visiblequests.TeamModeQuestFilterSource import de.westnordost.streetcomplete.util.ktx.format +import de.westnordost.streetcomplete.util.ktx.updatesWithPermissionChanges import de.westnordost.streetcomplete.util.logs.Log import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope @@ -117,9 +118,9 @@ class AutoSyncer( coroutineScope.launch { owner.repeatOnLifecycle(Lifecycle.State.STARTED) { val request = LocationRequest(LocationAccuracy.High, 30.seconds, 100.meters) - locationProvider.updates(request).collect { locationEvent -> - if (locationEvent is LocationEvent.Fix) { - val (position, accuracy) = locationEvent.location.position + locationProvider.updatesWithPermissionChanges(request).collect { locationEvent -> + if (locationEvent is LocationEvent.Update) { + val (position, accuracy) = locationEvent.measurement if (accuracy == null || accuracy < 300.meters) { pos = LatLon(position.latitude, position.longitude) triggerAutoDownload() diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/ktx/Location.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/ktx/Location.kt index a6ee3873989..1819e93b595 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/ktx/Location.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/ktx/Location.kt @@ -2,14 +2,28 @@ package de.westnordost.streetcomplete.util.ktx import de.westnordost.streetcomplete.data.location.Location import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import org.maplibre.compose.location.LocationEvent +import org.maplibre.compose.location.LocationProvider +import org.maplibre.compose.location.LocationRequest import org.maplibre.spatialk.units.International +import kotlin.time.TimeSource -fun org.maplibre.compose.location.Location.toLocation(): Location = +private val locationTimeMark = TimeSource.Monotonic.markNow() + +fun LocationEvent.Update.toLocation(): Location = Location( - position = position.value.toLatLon(), - accuracy = position.accuracy?.toFloat(International.Meters) ?: 0f, - elapsedDuration = timestamp.elapsedNow(), + position = measurement.position.toLatLon(), + accuracy = measurement.horizontalAccuracy?.toFloat(International.Meters) ?: 0f, + elapsedDuration = locationTimeMark.elapsedNow() - measurementMark.elapsedNow(), ) fun org.maplibre.spatialk.geojson.Position.toLatLon(): LatLon = LatLon(latitude, longitude) + +// TODO remove after upgrading to a version containing https://github.com/maplibre/maplibre-compose/pull/1393 +@OptIn(ExperimentalCoroutinesApi::class) +fun LocationProvider.updatesWithPermissionChanges(request: LocationRequest): Flow = + permission.flatMapLatest { updates(request) } diff --git a/app/src/commonTest/kotlin/de/westnordost/streetcomplete/data/location/RecentLocationsTest.kt b/app/src/commonTest/kotlin/de/westnordost/streetcomplete/data/location/RecentLocationsTest.kt index 6a6acdef84f..798a4120819 100644 --- a/app/src/commonTest/kotlin/de/westnordost/streetcomplete/data/location/RecentLocationsTest.kt +++ b/app/src/commonTest/kotlin/de/westnordost/streetcomplete/data/location/RecentLocationsTest.kt @@ -2,11 +2,17 @@ package de.westnordost.streetcomplete.data.location import de.westnordost.streetcomplete.data.osm.mapdata.LatLon import de.westnordost.streetcomplete.util.ktx.asSequenceOfPairs +import de.westnordost.streetcomplete.util.ktx.toLocation import de.westnordost.streetcomplete.util.math.translate +import org.maplibre.compose.location.LocationEvent +import org.maplibre.compose.location.LocationMeasurement +import org.maplibre.spatialk.geojson.Position import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds +import kotlin.time.Instant +import kotlin.time.TimeSource class RecentLocationsTest { @Test fun `getAll returns nothing when empty`() { @@ -25,6 +31,23 @@ class RecentLocationsTest { ) } + @Test fun `accepts first location with zero elapsedDuration`() { + val r = RecentLocations(10.seconds, 1.0, 1.seconds) + val location = Location(LatLon(0.0, 0.0), 1f, 0.seconds) + r.add(location) + + assertEquals(location, r.getAll().toList().single()) + } + + @Test fun `accepts first location with negative elapsedDuration`() { + // A cached fix can predate the adapter's monotonic time origin. + val r = RecentLocations(10.seconds, 1.0, 1.seconds) + val location = Location(LatLon(0.0, 0.0), 1f, (-1).seconds) + r.add(location) + + assertEquals(location, r.getAll().toList().single()) + } + @Test fun `getAll returns ordered by elapsedDuration descending`() { val r = RecentLocations(10.seconds, 1.0, 1.seconds) val l1 = Location(LatLon(0.0, 0.0), 1f, 1.seconds) @@ -37,6 +60,25 @@ class RecentLocationsTest { } } + @Test fun `retains a fresh fix after an older converted fix`() { + // Using a fix's age as its timestamp made RecentLocations discard newer fixes. + val r = RecentLocations(60.seconds, 1.0, 1.seconds) + val mark = TimeSource.Monotonic.markNow() + val measurement = LocationMeasurement( + position = Position(2.0, 1.0), + measuredAt = Instant.fromEpochSeconds(100), + ) + val older = LocationEvent.Update(measurement, mark - 10.seconds).toLocation() + val newer = LocationEvent.Update( + measurement.copy(position = Position(3.0, 1.0), measuredAt = Instant.fromEpochSeconds(110)), + mark, + ).toLocation() + r.add(older) + r.add(newer) + + assertEquals(listOf(newer, older), r.getAll().toList()) + } + @Test fun `does not add older locations`() { val r = RecentLocations(10.seconds, 1.0, 1.seconds) val l1 = Location(LatLon(0.0, 0.0), 1f, 10.seconds) diff --git a/app/src/iosMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.ios.kt b/app/src/iosMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.ios.kt index b4bac024208..c26c1f6e622 100644 --- a/app/src/iosMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.ios.kt +++ b/app/src/iosMain/kotlin/de/westnordost/streetcomplete/ui/util/Location.ios.kt @@ -1,10 +1,14 @@ package de.westnordost.streetcomplete.ui.util import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import org.maplibre.compose.location.IosLocationProvider import org.maplibre.compose.location.LocationProvider -@Composable actual fun rememberDefaultLocationProvider(): LocationProvider = remember { - IosLocationProvider() +// Identical to the upstream maplibre-compose implementation. +@Composable actual fun rememberDefaultLocationProvider(): LocationProvider { + val provider = remember { IosLocationProvider() } + DisposableEffect(provider) { onDispose { provider.close() } } + return provider }