Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import org.maplibre.compose.demoapp.DemoState
import org.maplibre.compose.demoapp.design.CardColumn
import org.maplibre.compose.gms.rememberFusedLocationProvider
import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationRequest
import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.material3.LocationPuckDefaults
import org.maplibre.spatialk.units.extensions.degrees

object GmsLocationDemo : Demo {
override val name = "Gms Location"
Expand All @@ -32,19 +34,31 @@ object GmsLocationDemo : Demo {
// this if _is_ a permission check Lint just doesn't know that
@SuppressLint("MissingPermission")
if (state.locationPermissionState.hasPermission) {
val locationProvider = rememberFusedLocationProvider()
val locationProvider = rememberFusedLocationProvider(LocationRequest(orientation = true))
val locationState = rememberUserLocationState(locationProvider)

LocationPuck(
idPrefix = "gms-location",
locationState = locationState,
location = locationState.location,
bearing =
locationState.location?.let { location ->
val courseAccuracy = location.course?.inaccuracy ?: 180.degrees
val orientationAccuracy = location.orientation?.inaccuracy ?: 180.degrees
if (courseAccuracy < orientationAccuracy) {
location.course
} else {
location.orientation
}
},
cameraState = state.cameraState,
accuracyThreshold = 0f,
colors = LocationPuckDefaults.colors(),
onClick = { location ->
locationClickedCount++
coroutineScope.launch {
state.cameraState.animateTo(CameraPosition(target = location.position, zoom = 16.0))
state.cameraState.animateTo(
CameraPosition(target = location.position!!.position, zoom = 16.0)
)
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.maplibre.compose.demoapp.demos.StyleSelectorDemo
import org.maplibre.compose.demoapp.demos.UserLocationDemo
import org.maplibre.compose.demoapp.util.Platform
import org.maplibre.compose.demoapp.util.PlatformFeature
import org.maplibre.compose.location.LocationRequest
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberNullLocationProvider
Expand Down Expand Up @@ -127,7 +128,7 @@ fun rememberDemoState(): DemoState {
val locationProvider =
key(locationPermissionState.hasPermission) {
if (locationPermissionState.hasPermission) {
rememberDefaultLocationProvider()
rememberDefaultLocationProvider(request = LocationRequest(orientation = true))
} else {
rememberNullLocationProvider()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ object UserLocationDemo : Demo {
if (!isOpen) return

LocationTrackingEffect(
locationState = state.locationState,
location = state.locationState.location,
enabled = trackLocation,
trackBearing = bearingUpdate == BearingUpdate.TRACK_LOCATION,
) {
Expand All @@ -58,6 +58,7 @@ object UserLocationDemo : Demo {
BearingUpdate.IGNORE -> GestureOptions.PositionLocked
BearingUpdate.ALWAYS_NORTH -> GestureOptions.ZoomOnly
BearingUpdate.TRACK_LOCATION -> GestureOptions.ZoomOnly
else -> TODO()
}
} else {
GestureOptions.Standard
Expand All @@ -74,11 +75,12 @@ object UserLocationDemo : Demo {

LocationPuck(
idPrefix = "user-location",
locationState = state.locationState,
location = state.locationState.location,
cameraState = state.cameraState,
accuracyThreshold = 0f,
showBearing = bearingUpdate != BearingUpdate.IGNORE,
showBearingAccuracy = bearingUpdate != BearingUpdate.IGNORE,
bearing =
if (bearingUpdate != BearingUpdate.IGNORE) state.locationState.location?.orientation
else null,
colors = LocationPuckDefaults.colors(),
onClick = { location ->
locationClickedCount++
Expand All @@ -91,6 +93,7 @@ object UserLocationDemo : Demo {
trackLocation = false
BearingUpdate.IGNORE
}
else -> TODO()
}
} else {
bearingUpdate = BearingUpdate.TRACK_LOCATION
Expand All @@ -100,7 +103,8 @@ object UserLocationDemo : Demo {
state.cameraState.position =
CameraPosition(
target =
state.locationState.location?.position ?: state.cameraState.position.target,
state.locationState.location?.position?.position
?: state.cameraState.position.target,
zoom = 16.0,
)
}
Expand All @@ -127,6 +131,8 @@ object UserLocationDemo : Demo {
BearingUpdate.IGNORE -> "ignoring bearing"
BearingUpdate.ALWAYS_NORTH -> "locked to north bearing"
BearingUpdate.TRACK_LOCATION -> "bearing"
BearingUpdate.TRACK_COURSE -> "course"
BearingUpdate.TRACK_ORIENTATION -> "orientation"
}
)
} else {
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ playServices-location = { module = "com.google.android.gms:play-services-locatio
simplejni-annotations = { module = "io.github.gershnik:smjni-jnigen-annotations", version.ref = "simplejni" }
simplejni-kprocessor = { module = "io.github.gershnik:smjni-jnigen-kprocessor", version.ref = "simplejni" }
spatialk-geojson = { group = "org.maplibre.spatialk", name = "geojson", version.ref = "spatialk" }
spatialk-units = { group = "org.maplibre.spatialk", name = "units", version.ref = "spatialk" }

[plugins]
android-application = { id = "com.android.application", version.ref = "gradle-android" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import com.google.android.gms.location.DeviceOrientation
import com.google.android.gms.location.DeviceOrientationRequest
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.FusedOrientationProviderClient
import com.google.android.gms.location.Granularity
import com.google.android.gms.location.LastLocationRequest
import com.google.android.gms.location.LocationAvailability
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationRequest as GMSLocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import java.util.concurrent.Executors
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
Expand All @@ -25,10 +30,15 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.zip
import kotlinx.coroutines.tasks.await
import org.maplibre.compose.location.BearingMeasurement
import org.maplibre.compose.location.Location
import org.maplibre.compose.location.LocationProvider
import org.maplibre.compose.location.LocationRequest
import org.maplibre.compose.location.asMapLibreLocation
import org.maplibre.spatialk.units.Bearing
import org.maplibre.spatialk.units.extensions.degrees

/**
* A [LocationProvider] based on a [LocationRequest] for [FusedLocationProviderClient]
Expand All @@ -43,15 +53,32 @@ public class FusedLocationProvider
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
)
constructor(
private val request: LocationRequest,
private val locationClient: FusedLocationProviderClient,
private val locationRequest: LocationRequest,
private val orientationClient: FusedOrientationProviderClient,
private val locationRequest: GMSLocationRequest,
coroutineScope: CoroutineScope,
sharingStarted: SharingStarted,
) : LocationProvider {
@Suppress("JoinDeclarationAndAssignment") // because of @RequiresPermission
override val location: StateFlow<Location?>

init {
val orientation = callbackFlow {
val request =
DeviceOrientationRequest.Builder(
locationRequest.intervalMillis.milliseconds.inWholeMicroseconds
)
.build()
val callback: (DeviceOrientation) -> Unit = { orientation ->
trySend(orientation.headingDegrees to orientation.headingErrorDegrees)
}

orientationClient.requestOrientationUpdates(request, dispatcher.executor, callback)

awaitClose { orientationClient.removeOrientationUpdates(callback) }
}

location =
callbackFlow {
val callback =
Expand Down Expand Up @@ -80,6 +107,20 @@ constructor(

awaitClose { locationClient.removeLocationUpdates(callback) }
}
.let { flow ->
if (!request.orientation) {
return@let flow
}
flow.zip(orientation) { location, (orientation, orientationAccuracy) ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zip is not the right operator here. If we never get a orientation, we should still emit a location without it, so we should use combine(orientation.sample(1.seconds)) to slow down orientation to the level of the location flow, but still emit if orientation never emits or is slower.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combine also doesn't emit until all flows have emitted at least once. Is it even possible to not get any orientation values? Other than faulty hardware?

Guess it doesn't matter if we split orientation into its own provider

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but you can force the orientation flow to emit at least once, by calling send(null) similar to how the location flow calls send(lastLocation). But yeah, doesn't matter, if we split it

(location ?: Location(timestamp = TimeSource.Monotonic.markNow())).copy(
orientation =
BearingMeasurement(
bearing = Bearing.North + orientation.toDouble().degrees,
inaccuracy = orientationAccuracy.toDouble().degrees,
)
)
}
}
.stateIn(coroutineScope, sharingStarted, null)
}

Expand All @@ -98,12 +139,15 @@ constructor(
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
)
public fun rememberFusedLocationProvider(
locationRequest: LocationRequest = defaultLocationRequest,
request: LocationRequest,
locationRequest: GMSLocationRequest = defaultLocationRequest,
context: Context = LocalContext.current,
): FusedLocationProvider {
val locationClient =
remember(context) { LocationServices.getFusedLocationProviderClient(context) }
return rememberFusedLocationProvider(locationClient, locationRequest)
val orientationClient =
remember(context) { LocationServices.getFusedOrientationProviderClient(context) }
return rememberFusedLocationProvider(request, locationClient, orientationClient, locationRequest)
}

/**
Expand All @@ -115,14 +159,18 @@ public fun rememberFusedLocationProvider(
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
)
public fun rememberFusedLocationProvider(
request: LocationRequest,
fusedLocationProviderClient: FusedLocationProviderClient,
locationRequest: LocationRequest = defaultLocationRequest,
fusedOrientationProviderClient: FusedOrientationProviderClient,
locationRequest: GMSLocationRequest = defaultLocationRequest,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000),
): FusedLocationProvider {
return remember(fusedLocationProviderClient) {
FusedLocationProvider(
request = request,
locationClient = fusedLocationProviderClient,
orientationClient = fusedOrientationProviderClient,
locationRequest = locationRequest,
coroutineScope = coroutineScope,
sharingStarted = sharingStarted,
Expand All @@ -131,6 +179,6 @@ public fun rememberFusedLocationProvider(
}

private val defaultLocationRequest =
LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000)
GMSLocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000)
.setMinUpdateIntervalMillis(1000)
.build()
1 change: 1 addition & 0 deletions lib/maplibre-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ kotlin {
implementation(libs.lifecycle.runtime.compose)
api(libs.kermit)
api(libs.spatialk.geojson)
api(libs.spatialk.units)
}

// used to share some implementation on targets where Compose UI is backed by Skia directly
Expand Down
Loading
Loading