Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -24,8 +24,8 @@ 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.UserLocationState
import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberNullLocationProvider
import org.maplibre.compose.location.rememberSensorEnhancedLocationProvider
import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.map.GestureOptions
import org.maplibre.compose.map.OrnamentOptions
Expand Down Expand Up @@ -59,7 +59,7 @@ class MapManipulationState {

class OrnamentOptionsState {
var isMaterial3ControlsEnabled by
mutableStateOf(PlatformFeature.InteropBlending in Platform.supportedFeatures)
mutableStateOf(PlatformFeature.InteropBlending in Platform.supportedFeatures)
}

class DemoState(
Expand Down Expand Up @@ -127,7 +127,7 @@ fun rememberDemoState(): DemoState {
val locationProvider =
key(locationPermissionState.hasPermission) {
if (locationPermissionState.hasPermission) {
rememberDefaultLocationProvider()
rememberSensorEnhancedLocationProvider()
} else {
rememberNullLocationProvider()
}
Expand All @@ -145,4 +145,5 @@ interface LocationPermissionState {
fun requestPermission()
}

@Composable expect fun rememberLocationPermissionState(): LocationPermissionState
@Composable
expect fun rememberLocationPermissionState(): LocationPermissionState
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package org.maplibre.compose.location

import android.Manifest
import android.content.Context
import android.content.Context.SENSOR_SERVICE
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.annotation.RequiresPermission
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn

/**
* A [LocationProvider] that enhances an existing [LocationProvider] with bearing information
* derived from the device's rotation vector sensor.
*
* This class listens to the `Sensor.TYPE_ROTATION_VECTOR` to get the device's orientation. It then
* combines this sensor-derived bearing with the location data from the wrapped [locationProvider].
*
* The sensor-based bearing is only used if its accuracy is better than the bearing accuracy
* provided by the original location provider.
*
* @param context The application context, used to access the `SensorManager`.
* @param locationProvider The underlying [LocationProvider] to be enhanced.
* @param coroutineScope The [CoroutineScope] in which the location and bearing flows are combined.
* @param sharingStarted The strategy for starting and stopping the collection of the location flow.
* Defaults to [SharingStarted.WhileSubscribed] with a 1-second stop timeout.
* @throws IllegalStateException if the rotation vector sensor is not available on the device.
*/
public class AndroidSensorEnhancedLocationProvider(
context: Context,
locationProvider: LocationProvider,
coroutineScope: CoroutineScope,
sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000),
) : LocationProvider {
private val sensorManager = context.getSystemService(SENSOR_SERVICE) as SensorManager

private fun accuracyToDegrees(accuracy: Int): Double =
when (accuracy) {
SensorManager.SENSOR_STATUS_ACCURACY_HIGH -> 5.0
SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM -> 15.0
SensorManager.SENSOR_STATUS_ACCURACY_LOW -> 45.0
SensorManager.SENSOR_STATUS_UNRELIABLE -> 180.0
else -> 180.0
}

private val bearing: Flow<Pair<Double, Double>> = callbackFlow {
val rotationMatrix = FloatArray(9)
val orientationAngles = FloatArray(3)
var accuracyDegrees = accuracyToDegrees(SensorManager.SENSOR_STATUS_UNRELIABLE)

val listener =
object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type == Sensor.TYPE_ROTATION_VECTOR) {
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
SensorManager.getOrientation(rotationMatrix, orientationAngles)

trySend(Math.toDegrees(orientationAngles[0].toDouble()) to accuracyDegrees)
}
}

override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
accuracyDegrees = accuracyToDegrees(accuracy)
}
}

val sensor =
sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
?: throw IllegalStateException("Rotation vector sensor is not available")

sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)

awaitClose { sensorManager.unregisterListener(listener) }
}

override val location: StateFlow<Location?> =
locationProvider.location
.combine(bearing) { location, (sensorBearing, sensorAccuracy) ->
val bearingAccuracy = location?.bearingAccuracy
if (bearingAccuracy != null && bearingAccuracy > sensorAccuracy) {
location.copy(bearing = sensorBearing, accuracy = sensorAccuracy)
} else {
location
}
}
.stateIn(coroutineScope, sharingStarted, null)
}

@Composable
@RequiresPermission(
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
)
public actual fun rememberSensorEnhancedLocationProvider(
locationProvider: LocationProvider
): LocationProvider {
return rememberAndroidSensorEnhancedLocationProvider(locationProvider = locationProvider)
}

/**
* Create and remember an [AndroidSensorEnhancedLocationProvider], a [LocationProvider] that
* enhances an existing [LocationProvider] with bearing information derived from the device's
* rotation vector sensor.
*/
@Composable
@RequiresPermission(
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
)
public fun rememberAndroidSensorEnhancedLocationProvider(
locationProvider: LocationProvider,
context: Context = LocalContext.current,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000),
): AndroidSensorEnhancedLocationProvider {
return remember(context, locationProvider, coroutineScope, sharingStarted) {
AndroidSensorEnhancedLocationProvider(
context = context,
locationProvider = locationProvider,
coroutineScope = coroutineScope,
sharingStarted = sharingStarted,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,14 @@ public expect fun rememberDefaultLocationProvider(
public fun rememberNullLocationProvider(): LocationProvider {
return remember { NullLocationProvider() }
}

/**
* Creates and remembers a [LocationProvider] that enhances another [LocationProvider] with bearing
* information from the device's orientation sensors.
*
* @return A new [LocationProvider] that provides sensor-enhanced bearing information.
*/
@Composable
public expect fun rememberSensorEnhancedLocationProvider(
locationProvider: LocationProvider = rememberDefaultLocationProvider()
): LocationProvider
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ public actual fun rememberDefaultLocationProvider(
): LocationProvider {
throw NotImplementedError("no default implementation for desktop")
}

@Composable
public actual fun rememberSensorEnhancedLocationProvider(
locationProvider: LocationProvider
): LocationProvider {
TODO()
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,11 @@ public fun rememberIosLocationProvider(
)
}
}

@Composable
public actual fun rememberSensorEnhancedLocationProvider(
locationProvider: LocationProvider
): LocationProvider {
// TODO: Implement sensor enhanced location provider
return locationProvider
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,12 @@ public actual fun rememberDefaultLocationProvider(
desiredAccuracy: DesiredAccuracy,
minDistanceMeters: Double,
): LocationProvider {
throw NotImplementedError("no default implementation for desktop")
throw NotImplementedError("no default implementation for web")
}

@Composable
public actual fun rememberSensorEnhancedLocationProvider(
locationProvider: LocationProvider
): LocationProvider {
TODO()
}
Loading