Skip to content

Commit

Permalink
Merge pull request #14 from Rallista/location-tracking-camera
Browse files Browse the repository at this point in the history
Location tracking camera
  • Loading branch information
Archdoog authored Apr 15, 2024
2 parents ab12d9c + 7c69b6c commit 0315f3f
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 102 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
android {
namespace = "com.maplibre.compose"
compileSdk = 34
version = "0.0.4"
version = "0.0.5"

defaultConfig {
minSdk = 25
Expand Down
124 changes: 124 additions & 0 deletions compose/src/main/java/com/maplibre/compose/StaticLocationEngine.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.maplibre.compose

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


/**
* 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<Pair<Handler, LocationEngineCallback<LocationEngineResult>>> =
mutableListOf()

private var pendingIntentTimer: Timer? = null
private val pendingIntents: MutableList<PendingIntent> = mutableListOf()

override fun getLastLocation(callback: LocationEngineCallback<LocationEngineResult>) {
val loc = lastLocation
if (loc != null) {
callback.onSuccess(LocationEngineResult.create(loc))
} else {
callback.onFailure(Exception("No location set"))
}
}

override fun requestLocationUpdates(
request: LocationEngineRequest,
callback: LocationEngineCallback<LocationEngineResult>,
looper: Looper?
) {
// 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(
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(callback: LocationEngineCallback<LocationEngineResult>) {
callbacks.removeIf { it.second == callback }
if (callbacks.isEmpty()) {
callbackTimer?.cancel()
callbackTimer = null
}
}

override fun removeLocationUpdates(intent: PendingIntent?) {
pendingIntents.remove(intent)
if (pendingIntents.isEmpty()) {
pendingIntentTimer?.cancel()
pendingIntentTimer = null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,23 @@ data class MapViewCamera(
return CameraPosition(
target = LatLng(state.latitude, state.longitude),
zoom = zoom,
pitch = pitch,
bearing = direction,
trackingMode = CameraTrackingMode.NONE
)
}
is CameraState.TrackingUserLocation -> {
return CameraPosition(
zoom = zoom,
tilt = 0.0,
pitch = pitch,
bearing = direction,
trackingMode = CameraTrackingMode.FOLLOW
)
}
is CameraState.TrackingUserLocationWithBearing -> {
return CameraPosition(
zoom = zoom,
tilt = 0.0,
pitch = pitch,
bearing = direction,
trackingMode = CameraTrackingMode.FOLLOW_WITH_BEARING
)
Expand Down
136 changes: 81 additions & 55 deletions compose/src/main/java/com/maplibre/compose/ramani/MapCameraUpdater.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
)
}
}
Expand All @@ -54,65 +56,89 @@ 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())

when (it.trackingMode) {
CameraTrackingMode.NONE -> {
if (map.locationComponent.isLocationComponentActivated) {
map.locationComponent.cameraMode = CameraMode.NONE
}

when (it.motionType) {
CameraMotionType.INSTANT -> map.moveCamera(cameraUpdate)

CameraMotionType.EASE -> map.easeCamera(
cameraUpdate,
it.animationDurationMs
)

CameraMotionType.FLY -> map.animateCamera(
cameraUpdate,
it.animationDurationMs
)
}
}
CameraTrackingMode.FOLLOW -> {
assert(map.locationComponent.isLocationComponentActivated)
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
}
}
update(cameraPosition.value) { updatedCameraPosition ->
cameraUpdate(map, updatedCameraPosition)
}
})
}

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<CameraPosition>
) : 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
}
CameraTrackingMode.FOLLOW -> {
assert(map.locationComponent.isLocationComponentActivated)
map.locationComponent.cameraMode = CameraMode.TRACKING
map.locationComponent.renderMode = RenderMode.COMPASS
when (cameraPosition.motionType) {
CameraMotionType.INSTANT -> map.moveCamera(cameraUpdate)

CameraMotionType.EASE -> map.easeCamera(
cameraUpdate,
cameraPosition.animationDurationMs
)

CameraMotionType.FLY -> map.animateCamera(
cameraUpdate,
cameraPosition.animationDurationMs
)
}
CameraTrackingMode.FOLLOW_WITH_BEARING -> {
assert(map.locationComponent.isLocationComponentActivated)
map.locationComponent.cameraMode = CameraMode.TRACKING_GPS
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.zoom,
cameraPosition.tilt
)
)
}
}
}
Expand Down

0 comments on commit 0315f3f

Please sign in to comment.