From fb46b3109704972819605f137d57c0d2cc623ddd Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Fri, 12 Apr 2024 23:37:13 +0900 Subject: [PATCH 1/5] First pass at fixing up location tracking --- .../example/support/StaticLocationEngine.kt | 1 + .../compose/camera/CameraPositionState.kt | 1 + .../maplibre/compose/camera/MapViewCamera.kt | 5 +- .../compose/ramani/MapCameraUpdater.kt | 60 +++++++++++++------ 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt b/app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt index a3d876f..fe0cb2c 100644 --- a/app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt +++ b/app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt @@ -8,6 +8,7 @@ import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback import com.mapbox.mapboxsdk.location.engine.LocationEngineRequest import com.mapbox.mapboxsdk.location.engine.LocationEngineResult +// TODO: Port this from what I have in Ferrostar today class StaticLocationEngine( private val center: Location = Location("StaticLocationEngine").apply { latitude = 37.7749 diff --git a/compose/src/main/java/com/maplibre/compose/camera/CameraPositionState.kt b/compose/src/main/java/com/maplibre/compose/camera/CameraPositionState.kt index 6494d2c..f93c19f 100644 --- a/compose/src/main/java/com/maplibre/compose/camera/CameraPositionState.kt +++ b/compose/src/main/java/com/maplibre/compose/camera/CameraPositionState.kt @@ -30,6 +30,7 @@ enum class CameraTrackingMode : Parcelable { fun fromMapbox(cameraMode: Int): CameraTrackingMode { return when (cameraMode) { com.mapbox.mapboxsdk.location.modes.CameraMode.TRACKING -> FOLLOW + com.mapbox.mapboxsdk.location.modes.CameraMode.TRACKING_GPS -> FOLLOW_WITH_BEARING com.mapbox.mapboxsdk.location.modes.CameraMode.TRACKING_COMPASS -> FOLLOW_WITH_BEARING else -> NONE } diff --git a/compose/src/main/java/com/maplibre/compose/camera/MapViewCamera.kt b/compose/src/main/java/com/maplibre/compose/camera/MapViewCamera.kt index c2c1619..27cc3c0 100644 --- a/compose/src/main/java/com/maplibre/compose/camera/MapViewCamera.kt +++ b/compose/src/main/java/com/maplibre/compose/camera/MapViewCamera.kt @@ -92,6 +92,7 @@ data class MapViewCamera( return CameraPosition( target = LatLng(state.latitude, state.longitude), zoom = zoom, + pitch = pitch, bearing = direction, trackingMode = CameraTrackingMode.NONE ) @@ -99,7 +100,7 @@ data class MapViewCamera( is CameraState.TrackingUserLocation -> { return CameraPosition( zoom = zoom, - tilt = 0.0, + pitch = pitch, bearing = direction, trackingMode = CameraTrackingMode.FOLLOW ) @@ -107,7 +108,7 @@ data class MapViewCamera( is CameraState.TrackingUserLocationWithBearing -> { return CameraPosition( zoom = zoom, - tilt = 0.0, + pitch = pitch, bearing = direction, trackingMode = CameraTrackingMode.FOLLOW_WITH_BEARING ) diff --git a/compose/src/main/java/com/maplibre/compose/ramani/MapCameraUpdater.kt b/compose/src/main/java/com/maplibre/compose/ramani/MapCameraUpdater.kt index f5ee79e..a710bd5 100644 --- a/compose/src/main/java/com/maplibre/compose/ramani/MapCameraUpdater.kt +++ b/compose/src/main/java/com/maplibre/compose/ramani/MapCameraUpdater.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.MutableState import androidx.compose.runtime.currentComposer import com.mapbox.mapboxsdk.camera.CameraUpdateFactory +import com.mapbox.mapboxsdk.location.OnLocationCameraTransitionListener import com.mapbox.mapboxsdk.location.modes.CameraMode import com.mapbox.mapboxsdk.location.modes.RenderMode import com.mapbox.mapboxsdk.maps.MapboxMap @@ -32,13 +33,14 @@ internal fun MapCameraUpdater( // See stack overflow link below for more info. onCameraIdle( CameraPosition( - target = mapApplier.map.cameraPosition.target, - zoom = mapApplier.map.cameraPosition.zoom, - tilt = mapApplier.map.cameraPosition.tilt, - pitch = CameraPitch.Free, - bearing = mapApplier.map.cameraPosition.bearing, - trackingMode = newTrackingMode, - ) + target = mapApplier.map.cameraPosition.target, + zoom = mapApplier.map.cameraPosition.zoom, + tilt = mapApplier.map.cameraPosition.tilt, + // TODO: This should PROBABLY not be hard-coded? I'm also not really sure if pitch constraints are part of position... + pitch = CameraPitch.Free, + bearing = mapApplier.map.cameraPosition.bearing, + trackingMode = newTrackingMode, + ) ) } } @@ -54,44 +56,63 @@ internal fun MapCameraUpdater( }, update = { // This function is run any time the cameraPosition changes. // It applies an update from the parent to the Map (maintained by the MapApplier) - update(cameraPosition.value) { - val cameraUpdate = CameraUpdateFactory.newCameraPosition(it.toMapbox()) + update(cameraPosition.value) { updatedCameraPosition -> + val cameraUpdate = CameraUpdateFactory.newCameraPosition(updatedCameraPosition.toMapbox()) - when (it.trackingMode) { + // TODO: Unify this logic with compose node stuff below! + when (updatedCameraPosition.trackingMode) { CameraTrackingMode.NONE -> { if (map.locationComponent.isLocationComponentActivated) { map.locationComponent.cameraMode = CameraMode.NONE } - when (it.motionType) { + when (updatedCameraPosition.motionType) { CameraMotionType.INSTANT -> map.moveCamera(cameraUpdate) CameraMotionType.EASE -> map.easeCamera( cameraUpdate, - it.animationDurationMs + updatedCameraPosition.animationDurationMs ) CameraMotionType.FLY -> map.animateCamera( cameraUpdate, - it.animationDurationMs + updatedCameraPosition.animationDurationMs ) } } CameraTrackingMode.FOLLOW -> { assert(map.locationComponent.isLocationComponentActivated) + + // TODO: Selective updates only if stuff changed map.locationComponent.cameraMode = CameraMode.TRACKING map.locationComponent.renderMode = RenderMode.COMPASS } CameraTrackingMode.FOLLOW_WITH_BEARING -> { assert(map.locationComponent.isLocationComponentActivated) - map.locationComponent.cameraMode = CameraMode.TRACKING_GPS - map.locationComponent.renderMode = RenderMode.COMPASS + + // TODO: Selective updates only if stuff changed } } } }) } +private class CameraTransitionListener(val map: MapboxMap, val zoom: Double?, val tilt: Double?) : OnLocationCameraTransitionListener { + override fun onLocationCameraTransitionFinished(cameraMode: Int) { + zoom?.let { zoom -> + map.locationComponent.zoomWhileTracking(zoom) + } + + tilt?.let { tilt -> + map.locationComponent.tiltWhileTracking(tilt) + } + } + + override fun onLocationCameraTransitionCanceled(cameraMode: Int) { + // Do nothing + } +} + private class MapPropertiesNode( val map: MapboxMap, var cameraPosition: MutableState @@ -111,8 +132,13 @@ private class MapPropertiesNode( } CameraTrackingMode.FOLLOW_WITH_BEARING -> { assert(map.locationComponent.isLocationComponentActivated) - map.locationComponent.cameraMode = CameraMode.TRACKING_GPS - map.locationComponent.renderMode = RenderMode.COMPASS + + map.locationComponent.renderMode = RenderMode.GPS + if (map.locationComponent.cameraMode != CameraMode.TRACKING_GPS) { + map.locationComponent.setCameraMode( + CameraMode.TRACKING_GPS, + CameraTransitionListener(map, cameraPosition.value.zoom, cameraPosition.value.tilt)) + } } } } From d6099de9052e6b6e7e1ab7c4dba10ac031c0be92 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 15 Apr 2024 01:34:24 +0900 Subject: [PATCH 2/5] Refactor common code out --- .../compose/ramani/MapCameraUpdater.kt | 110 +++++++++--------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/compose/src/main/java/com/maplibre/compose/ramani/MapCameraUpdater.kt b/compose/src/main/java/com/maplibre/compose/ramani/MapCameraUpdater.kt index a710bd5..0cdf173 100644 --- a/compose/src/main/java/com/maplibre/compose/ramani/MapCameraUpdater.kt +++ b/compose/src/main/java/com/maplibre/compose/ramani/MapCameraUpdater.kt @@ -57,42 +57,7 @@ internal fun MapCameraUpdater( // This function is run any time the cameraPosition changes. // It applies an update from the parent to the Map (maintained by the MapApplier) update(cameraPosition.value) { updatedCameraPosition -> - val cameraUpdate = CameraUpdateFactory.newCameraPosition(updatedCameraPosition.toMapbox()) - - // TODO: Unify this logic with compose node stuff below! - when (updatedCameraPosition.trackingMode) { - CameraTrackingMode.NONE -> { - if (map.locationComponent.isLocationComponentActivated) { - map.locationComponent.cameraMode = CameraMode.NONE - } - - when (updatedCameraPosition.motionType) { - CameraMotionType.INSTANT -> map.moveCamera(cameraUpdate) - - CameraMotionType.EASE -> map.easeCamera( - cameraUpdate, - updatedCameraPosition.animationDurationMs - ) - - CameraMotionType.FLY -> map.animateCamera( - cameraUpdate, - updatedCameraPosition.animationDurationMs - ) - } - } - CameraTrackingMode.FOLLOW -> { - assert(map.locationComponent.isLocationComponentActivated) - - // TODO: Selective updates only if stuff changed - map.locationComponent.cameraMode = CameraMode.TRACKING - map.locationComponent.renderMode = RenderMode.COMPASS - } - CameraTrackingMode.FOLLOW_WITH_BEARING -> { - assert(map.locationComponent.isLocationComponentActivated) - - // TODO: Selective updates only if stuff changed - } - } + cameraUpdate(map, updatedCameraPosition) } }) } @@ -118,27 +83,62 @@ private class MapPropertiesNode( var cameraPosition: MutableState ) : MapNode { override fun onAttached() { - when (cameraPosition.value.trackingMode) { - CameraTrackingMode.NONE -> { - if (map.locationComponent.isLocationComponentActivated) { - map.locationComponent.cameraMode = CameraMode.NONE - } - map.cameraPosition = cameraPosition.value.toMapbox() + cameraUpdate(map, cameraPosition.value) + } +} + +private fun cameraUpdate(map: MapboxMap, cameraPosition: CameraPosition) { + val cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition.toMapbox()) + + when (cameraPosition.trackingMode) { + CameraTrackingMode.NONE -> { + if (map.locationComponent.isLocationComponentActivated) { + map.locationComponent.cameraMode = CameraMode.NONE + } + when (cameraPosition.motionType) { + CameraMotionType.INSTANT -> map.moveCamera(cameraUpdate) + + CameraMotionType.EASE -> map.easeCamera( + cameraUpdate, + cameraPosition.animationDurationMs + ) + + CameraMotionType.FLY -> map.animateCamera( + cameraUpdate, + cameraPosition.animationDurationMs + ) } - CameraTrackingMode.FOLLOW -> { - assert(map.locationComponent.isLocationComponentActivated) - map.locationComponent.cameraMode = CameraMode.TRACKING - map.locationComponent.renderMode = RenderMode.COMPASS + } + + CameraTrackingMode.FOLLOW -> { + assert(map.locationComponent.isLocationComponentActivated) + + map.locationComponent.renderMode = RenderMode.COMPASS + if (map.locationComponent.cameraMode != CameraMode.TRACKING) { + map.locationComponent.setCameraMode( + CameraMode.TRACKING, + CameraTransitionListener( + map, + cameraPosition.zoom, + cameraPosition.tilt + ) + ) } - CameraTrackingMode.FOLLOW_WITH_BEARING -> { - assert(map.locationComponent.isLocationComponentActivated) - - map.locationComponent.renderMode = RenderMode.GPS - if (map.locationComponent.cameraMode != CameraMode.TRACKING_GPS) { - map.locationComponent.setCameraMode( - CameraMode.TRACKING_GPS, - CameraTransitionListener(map, cameraPosition.value.zoom, cameraPosition.value.tilt)) - } + } + + CameraTrackingMode.FOLLOW_WITH_BEARING -> { + assert(map.locationComponent.isLocationComponentActivated) + + map.locationComponent.renderMode = RenderMode.GPS + if (map.locationComponent.cameraMode != CameraMode.TRACKING_GPS) { + map.locationComponent.setCameraMode( + CameraMode.TRACKING_GPS, + CameraTransitionListener( + map, + cameraPosition.zoom, + cameraPosition.tilt + ) + ) } } } From 348e5f994b9de42a0bfbf5a2b99a4c37ad81c447 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 15 Apr 2024 01:35:43 +0900 Subject: [PATCH 3/5] Version bump --- README.md | 2 +- compose/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7daed1f..d761e6a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ In your `settings.gradle`. A personal access token (PAT) is required to access t In your app `build.gradle` ```groovy -implementation 'io.github.rallista:maplibre-compose:0.0.4' +implementation 'io.github.rallista:maplibre-compose:0.0.5' ``` ## Usage diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index 92899fb..4b61e19 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "com.maplibre.compose" compileSdk = 34 - version = "0.0.4" + version = "0.0.5" defaultConfig { minSdk = 25 From 1c8ef591fc25c853d2bef7a5011494828b414d74 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 15 Apr 2024 01:38:39 +0900 Subject: [PATCH 4/5] Update StaticLocationEngine to behave like the SwiftUI equivalent (which is actually useful in real apps) --- .../example/support/StaticLocationEngine.kt | 120 +++++++++++++++--- 1 file changed, 101 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt b/app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt index fe0cb2c..57f91b1 100644 --- a/app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt +++ b/app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt @@ -1,42 +1,124 @@ package com.maplibre.example.support import android.app.PendingIntent +import android.content.Intent import android.location.Location +import android.location.LocationManager +import android.os.Handler import android.os.Looper import com.mapbox.mapboxsdk.location.engine.LocationEngine import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback import com.mapbox.mapboxsdk.location.engine.LocationEngineRequest import com.mapbox.mapboxsdk.location.engine.LocationEngineResult +import java.lang.Exception +import java.util.Timer +import java.util.TimerTask -// TODO: Port this from what I have in Ferrostar today -class StaticLocationEngine( - private val center: Location = Location("StaticLocationEngine").apply { - latitude = 37.7749 - longitude = -122.4194 - } -): LocationEngine { + +/** + * A simple class that provides static location updates to a MapLibre view. + * + * This is not driven by a location provider (such as the Google fused client), but rather by + * updates provided one at a time. + * + * Beyond the obvious use case in testing and Compose previews, this is also useful if you are doing + * some processing of raw location data (ex: determining whether to snap locations to a road) and + * selectively passing the updates on to the map view. You can provide a new location by setting the + * ``lastLocation`` property. + * + * This class does not ever perform any authorization checks. That is the responsibility of the + * caller. + */ +class StaticLocationEngine : LocationEngine { + @Volatile + var lastLocation: Location? = null + @Synchronized set + + private var callbackTimer: Timer? = null + private val callbacks: MutableList>> = + mutableListOf() + + private var pendingIntentTimer: Timer? = null + private val pendingIntents: MutableList = mutableListOf() override fun getLastLocation(callback: LocationEngineCallback) { - callback.onSuccess(LocationEngineResult.create(center)) + val loc = lastLocation + if (loc != null) { + callback.onSuccess(LocationEngineResult.create(loc)) + } else { + callback.onFailure(Exception("No location set")) + } } override fun requestLocationUpdates( - p0: LocationEngineRequest, - p1: LocationEngineCallback, - p2: Looper? + request: LocationEngineRequest, + callback: LocationEngineCallback, + looper: Looper? ) { - // No action necessary - this is not a real location engine + // Register the callback + callbacks.add(Pair(Handler(looper ?: Looper.getMainLooper()), callback)) + if (callbackTimer == null) { + // If a timer isn't already running, create one + callbackTimer = + Timer().apply { + scheduleAtFixedRate( + object : TimerTask() { + override fun run() { + lastLocation?.let { + val result = LocationEngineResult.create(it) + for ((handler, callback) in callbacks) { + handler.post { callback.onSuccess(result) } + } + } + } + }, + 0, + 1000) + } + } } - override fun requestLocationUpdates(p0: LocationEngineRequest, p1: PendingIntent?) { - // No action necessary - this is not a real location engine + override fun requestLocationUpdates( + request: LocationEngineRequest, + pendingIntent: PendingIntent? + ) { + if (pendingIntent != null) { + pendingIntents.add(pendingIntent) + if (pendingIntentTimer == null) { + // If a timer isn't already running, create one + pendingIntentTimer = + Timer().apply { + scheduleAtFixedRate( + object : TimerTask() { + override fun run() { + lastLocation?.let { + for (intent in pendingIntents) { + val update = Intent() + update.putExtra(LocationManager.KEY_LOCATION_CHANGED, it) + } + } + } + }, + 0, + 1000) + } + } + } } - override fun removeLocationUpdates(p0: LocationEngineCallback) { - // No action necessary - this is not a real location engine + override fun removeLocationUpdates(callback: LocationEngineCallback) { + callbacks.removeIf { it.second == callback } + if (callbacks.isEmpty()) { + callbackTimer?.cancel() + callbackTimer = null + } } - override fun removeLocationUpdates(p0: PendingIntent?) { - // No action necessary - this is not a real location engine + override fun removeLocationUpdates(intent: PendingIntent?) { + pendingIntents.remove(intent) + if (pendingIntents.isEmpty()) { + pendingIntentTimer?.cancel() + pendingIntentTimer = null + } } -} \ No newline at end of file +} From 7c69b6c461eddae9f58855ae4051d0b0ca1a846f Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 15 Apr 2024 01:48:02 +0900 Subject: [PATCH 5/5] Move the StaticLocationEngine; didn't realize that was only in the example namespace --- .../main/java/com/maplibre/example/examples/CameraExample.kt | 3 +-- .../main/java/com/maplibre/compose}/StaticLocationEngine.kt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) rename {app/src/main/java/com/maplibre/example/support => compose/src/main/java/com/maplibre/compose}/StaticLocationEngine.kt (99%) diff --git a/app/src/main/java/com/maplibre/example/examples/CameraExample.kt b/app/src/main/java/com/maplibre/example/examples/CameraExample.kt index efaf4fd..ba40cc5 100644 --- a/app/src/main/java/com/maplibre/example/examples/CameraExample.kt +++ b/app/src/main/java/com/maplibre/example/examples/CameraExample.kt @@ -18,8 +18,7 @@ import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.MapView import com.maplibre.compose.camera.CameraState import com.maplibre.compose.rememberSaveableMapViewCamera -import com.maplibre.example.Main -import com.maplibre.example.support.StaticLocationEngine +import com.maplibre.compose.StaticLocationEngine import com.maplibre.example.support.locationPermissions import com.maplibre.example.support.rememberLocationPermissionLauncher diff --git a/app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt b/compose/src/main/java/com/maplibre/compose/StaticLocationEngine.kt similarity index 99% rename from app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt rename to compose/src/main/java/com/maplibre/compose/StaticLocationEngine.kt index 57f91b1..31dd807 100644 --- a/app/src/main/java/com/maplibre/example/support/StaticLocationEngine.kt +++ b/compose/src/main/java/com/maplibre/compose/StaticLocationEngine.kt @@ -1,4 +1,4 @@ -package com.maplibre.example.support +package com.maplibre.compose import android.app.PendingIntent import android.content.Intent