Skip to content

Commit 3a926f5

Browse files
committed
Sensor-enhanced location provider
Adds a LocationProvider that enhances another LocationProvider with the bearing values of the device rotation sensor. It provides accurate bearing values when the default location provider is unable to do so (commonly due to lack of movement).
1 parent 727bdef commit 3a926f5

File tree

6 files changed

+173
-5
lines changed

6 files changed

+173
-5
lines changed

demo-app/src/commonMain/kotlin/org/maplibre/compose/demoapp/DemoState.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import org.maplibre.compose.demoapp.demos.UserLocationDemo
2424
import org.maplibre.compose.demoapp.util.Platform
2525
import org.maplibre.compose.demoapp.util.PlatformFeature
2626
import org.maplibre.compose.location.UserLocationState
27-
import org.maplibre.compose.location.rememberDefaultLocationProvider
2827
import org.maplibre.compose.location.rememberNullLocationProvider
28+
import org.maplibre.compose.location.rememberSensorEnhancedLocationProvider
2929
import org.maplibre.compose.location.rememberUserLocationState
3030
import org.maplibre.compose.map.GestureOptions
3131
import org.maplibre.compose.map.OrnamentOptions
@@ -59,7 +59,7 @@ class MapManipulationState {
5959

6060
class OrnamentOptionsState {
6161
var isMaterial3ControlsEnabled by
62-
mutableStateOf(PlatformFeature.InteropBlending in Platform.supportedFeatures)
62+
mutableStateOf(PlatformFeature.InteropBlending in Platform.supportedFeatures)
6363
}
6464

6565
class DemoState(
@@ -127,7 +127,7 @@ fun rememberDemoState(): DemoState {
127127
val locationProvider =
128128
key(locationPermissionState.hasPermission) {
129129
if (locationPermissionState.hasPermission) {
130-
rememberDefaultLocationProvider()
130+
rememberSensorEnhancedLocationProvider()
131131
} else {
132132
rememberNullLocationProvider()
133133
}
@@ -145,4 +145,5 @@ interface LocationPermissionState {
145145
fun requestPermission()
146146
}
147147

148-
@Composable expect fun rememberLocationPermissionState(): LocationPermissionState
148+
@Composable
149+
expect fun rememberLocationPermissionState(): LocationPermissionState
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package org.maplibre.compose.location
2+
3+
import android.Manifest
4+
import android.content.Context
5+
import android.content.Context.SENSOR_SERVICE
6+
import android.hardware.Sensor
7+
import android.hardware.SensorEvent
8+
import android.hardware.SensorEventListener
9+
import android.hardware.SensorManager
10+
import androidx.annotation.RequiresPermission
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.runtime.remember
13+
import androidx.compose.runtime.rememberCoroutineScope
14+
import androidx.compose.ui.platform.LocalContext
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.channels.awaitClose
17+
import kotlinx.coroutines.flow.Flow
18+
import kotlinx.coroutines.flow.SharingStarted
19+
import kotlinx.coroutines.flow.StateFlow
20+
import kotlinx.coroutines.flow.callbackFlow
21+
import kotlinx.coroutines.flow.combine
22+
import kotlinx.coroutines.flow.stateIn
23+
24+
/**
25+
* A [LocationProvider] that enhances an existing [LocationProvider] with bearing information
26+
* derived from the device's rotation vector sensor.
27+
*
28+
* This class listens to the `Sensor.TYPE_ROTATION_VECTOR` to get the device's orientation. It then
29+
* combines this sensor-derived bearing with the location data from the wrapped [locationProvider].
30+
*
31+
* The sensor-based bearing is only used if its accuracy is better than the bearing accuracy
32+
* provided by the original location provider.
33+
*
34+
* @param context The application context, used to access the `SensorManager`.
35+
* @param locationProvider The underlying [LocationProvider] to be enhanced.
36+
* @param coroutineScope The [CoroutineScope] in which the location and bearing flows are combined.
37+
* @param sharingStarted The strategy for starting and stopping the collection of the location flow.
38+
* Defaults to [SharingStarted.WhileSubscribed] with a 1-second stop timeout.
39+
* @throws IllegalStateException if the rotation vector sensor is not available on the device.
40+
*/
41+
public class AndroidSensorEnhancedLocationProvider(
42+
context: Context,
43+
locationProvider: LocationProvider,
44+
coroutineScope: CoroutineScope,
45+
sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000),
46+
) : LocationProvider {
47+
private val sensorManager = context.getSystemService(SENSOR_SERVICE) as SensorManager
48+
49+
private fun accuracyToDegrees(accuracy: Int): Double =
50+
when (accuracy) {
51+
SensorManager.SENSOR_STATUS_ACCURACY_HIGH -> 5.0
52+
SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM -> 15.0
53+
SensorManager.SENSOR_STATUS_ACCURACY_LOW -> 45.0
54+
SensorManager.SENSOR_STATUS_UNRELIABLE -> 180.0
55+
else -> 180.0
56+
}
57+
58+
private val bearing: Flow<Pair<Double, Double>> = callbackFlow {
59+
val rotationMatrix = FloatArray(9)
60+
val orientationAngles = FloatArray(3)
61+
var accuracyDegrees = accuracyToDegrees(SensorManager.SENSOR_STATUS_UNRELIABLE)
62+
63+
val listener =
64+
object : SensorEventListener {
65+
override fun onSensorChanged(event: SensorEvent?) {
66+
if (event?.sensor?.type == Sensor.TYPE_ROTATION_VECTOR) {
67+
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
68+
SensorManager.getOrientation(rotationMatrix, orientationAngles)
69+
70+
trySend(Math.toDegrees(orientationAngles[0].toDouble()) to accuracyDegrees)
71+
}
72+
}
73+
74+
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
75+
accuracyDegrees = accuracyToDegrees(accuracy)
76+
}
77+
}
78+
79+
val sensor =
80+
sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
81+
?: throw IllegalStateException("Rotation vector sensor is not available")
82+
83+
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
84+
85+
awaitClose { sensorManager.unregisterListener(listener) }
86+
}
87+
88+
override val location: StateFlow<Location?> =
89+
locationProvider.location
90+
.combine(bearing) { location, (sensorBearing, sensorAccuracy) ->
91+
val bearingAccuracy = location?.bearingAccuracy
92+
if (bearingAccuracy != null && bearingAccuracy > sensorAccuracy) {
93+
location.copy(bearing = sensorBearing, accuracy = sensorAccuracy)
94+
} else {
95+
location
96+
}
97+
}
98+
.stateIn(coroutineScope, sharingStarted, null)
99+
}
100+
101+
@Composable
102+
@RequiresPermission(
103+
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
104+
)
105+
public actual fun rememberSensorEnhancedLocationProvider(
106+
locationProvider: LocationProvider
107+
): LocationProvider {
108+
return rememberAndroidSensorEnhancedLocationProvider(locationProvider = locationProvider)
109+
}
110+
111+
/**
112+
* Create and remember an [AndroidSensorEnhancedLocationProvider], a [LocationProvider] that
113+
* enhances an existing [LocationProvider] with bearing information derived from the device's
114+
* rotation vector sensor.
115+
*/
116+
@Composable
117+
@RequiresPermission(
118+
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
119+
)
120+
public fun rememberAndroidSensorEnhancedLocationProvider(
121+
locationProvider: LocationProvider,
122+
context: Context = LocalContext.current,
123+
coroutineScope: CoroutineScope = rememberCoroutineScope(),
124+
sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000),
125+
): AndroidSensorEnhancedLocationProvider {
126+
return remember(context, locationProvider, coroutineScope, sharingStarted) {
127+
AndroidSensorEnhancedLocationProvider(
128+
context = context,
129+
locationProvider = locationProvider,
130+
coroutineScope = coroutineScope,
131+
sharingStarted = sharingStarted,
132+
)
133+
}
134+
}

lib/maplibre-compose/src/commonMain/kotlin/org/maplibre/compose/location/LocationProvider.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,14 @@ public expect fun rememberDefaultLocationProvider(
116116
public fun rememberNullLocationProvider(): LocationProvider {
117117
return remember { NullLocationProvider() }
118118
}
119+
120+
/**
121+
* Creates and remembers a [LocationProvider] that enhances another [LocationProvider] with bearing
122+
* information from the device's orientation sensors.
123+
*
124+
* @return A new [LocationProvider] that provides sensor-enhanced bearing information.
125+
*/
126+
@Composable
127+
public expect fun rememberSensorEnhancedLocationProvider(
128+
locationProvider: LocationProvider = rememberDefaultLocationProvider()
129+
): LocationProvider

lib/maplibre-compose/src/desktopMain/kotlin/org/maplibre/compose/location/DesktopLocationProvider.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,10 @@ public actual fun rememberDefaultLocationProvider(
1111
): LocationProvider {
1212
throw NotImplementedError("no default implementation for desktop")
1313
}
14+
15+
@Composable
16+
public actual fun rememberSensorEnhancedLocationProvider(
17+
locationProvider: LocationProvider
18+
): LocationProvider {
19+
TODO()
20+
}

lib/maplibre-compose/src/iosMain/kotlin/org/maplibre/compose/location/IosLocationProvider.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,11 @@ public fun rememberIosLocationProvider(
108108
)
109109
}
110110
}
111+
112+
@Composable
113+
public actual fun rememberSensorEnhancedLocationProvider(
114+
locationProvider: LocationProvider
115+
): LocationProvider {
116+
// TODO: Implement sensor enhanced location provider
117+
return locationProvider
118+
}

lib/maplibre-compose/src/jsMain/kotlin/org/maplibre/compose/location/WebLocationProvider.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,12 @@ public actual fun rememberDefaultLocationProvider(
99
desiredAccuracy: DesiredAccuracy,
1010
minDistanceMeters: Double,
1111
): LocationProvider {
12-
throw NotImplementedError("no default implementation for desktop")
12+
throw NotImplementedError("no default implementation for web")
13+
}
14+
15+
@Composable
16+
public actual fun rememberSensorEnhancedLocationProvider(
17+
locationProvider: LocationProvider
18+
): LocationProvider {
19+
TODO()
1320
}

0 commit comments

Comments
 (0)