Skip to content

Add orientation provider#737

Open
jgillich wants to merge 19 commits intomaplibre:mainfrom
jgillich:orientation
Open

Add orientation provider#737
jgillich wants to merge 19 commits intomaplibre:mainfrom
jgillich:orientation

Conversation

@jgillich
Copy link
Collaborator

@jgillich jgillich commented Jan 5, 2026

Continuation of #712

  • New orientation provider returns device orientation
  • rememberUserLocationState will return orientation if orientationProvider is provided
  • Updated demos to display orientation
  • Renamed location bearing to course
  • Added measurement types (value+accuracy) for location members
  • LocationPuck accepts BearingMeasurement to support both course and orientation
  • Use spatialk/units for location provider minDistance

TODO:

  • GMS Demo crashes when you hit close
  • FusedOrientationProvider emits much more frequently than the provided MinUpdateInterval - should we .sample? I wonder if FusedLocationProvider does this too
  • iOS

* New orientation provider returns device orientation
* rememberUserLocationState will return orientation if orientationProvider is provided
* Updated demos to display orientation
* Renamed location bearing to course
* Added measurement types (value+accuracy) for location members
* LocationPuck accepts BearingMeasurement to support both course and orientation
* Use spatialk/units for location provider minDistance
@sargunv
Copy link
Collaborator

sargunv commented Jan 7, 2026

Thanks again for working on this! Will hold off on reviewing as it's in draft mode, but feel free to @ me anytime when you'd like a review

object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.locations.forEach { trySendBlocking(it.asMapLibreLocation()).getOrThrow() }
result.locations.forEach { trySend(it.asMapLibreLocation()) }
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This had the potential to throw a CancellationException (ForgottenCoroutineScopeException) if values are sent after cancellation. As far as I know, there are only two failure scenarios, either the channel is closed or the channel is not being collected and the buffer exceeds capacity, neither of which justify throwing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems reasonable

public fun rememberUserLocationState(
locationProvider: LocationProvider,
orientationProvider: OrientationProvider? = null,
samplePeriod: Duration? = 1.seconds,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added a sampling period here to avoid doubling recompositions when orientationProvider is used. It is up to the user to sync up all the periods, potential pitfall if they miss this one. Not sure if it would be better to remove the default value entirely

Copy link
Contributor

Choose a reason for hiding this comment

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

I think a better solution would be to add a

val samplePeriod: Duration

to the *Provider interfaces. The you can just use the max of those for the flow.sample call (or the code I suggest below).

But after thinking about it, I'm actually not sure what you're trying to avoid here. The trivial solution (a second collectAsStateWithLifecycle) shouldn't lead to unnecessary recompositions, assuming you want a recomposition every time either location or orientation changes. That recomposition should also just affect the composables that read location or orientation from the returned UserLocationState, i.e. normally just LocationPuck.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If you have location at 1s and orientation at 1s, your UI will recompose every 0.5s on average. That is unnecessary for most apps. In my navigation app, I have a bunch of effects that respond to location changes, and keeping energy usage as low as possible is important to me. Although it is possible that this ordeal of combine/sample uses more energy than not doing it at all, so I'll see about optimizing it

Copy link
Contributor

Choose a reason for hiding this comment

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

If you really want to reduce energy consumption, I would tune the parameters of the location/orientation providers so they emit fewer or less accurate updates in the first place. The most expensive part (in terms of energy) is definitely reading and processing the sensor values.

@jgillich jgillich marked this pull request as ready for review January 11, 2026 23:11
@jgillich jgillich requested a review from sargunv January 11, 2026 23:11
@kodebach kodebach mentioned this pull request Jan 25, 2026
@jgillich
Copy link
Collaborator Author

jgillich commented Feb 1, 2026

This is ready for review btw @sargunv (and @kodebach in case you want to)

Copy link
Contributor

@kodebach kodebach left a comment

Choose a reason for hiding this comment

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

I think making UserLocationState a parameter of LocationPuck was actually a mistake and we should fix that. The new bearing parameter would defeat the point of passing location as a state object anyway.

object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.locations.forEach { trySendBlocking(it.asMapLibreLocation()).getOrThrow() }
result.locations.forEach { trySend(it.asMapLibreLocation()) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems reasonable

accuracyThreshold: Float = 50f,
colors: LocationPuckColors = LocationPuckColors(),
sizes: LocationPuckSizes = LocationPuckSizes(),
showBearing: Boolean = true,
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need showBearing anymore, if bearing is a separate parameter. Instead of showBearing = false you can just pass bearing = null now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Two reasons for this:

  1. We also have showBearingAccuracy: Boolean, for consistency's sake it would be odd to turn off one via null and the other via false. We could remove both of these flags and only show them unless null, but:
  2. We may, at some point in the future, want to indicate that bearing is unknown rather than disabled

public actual fun rememberDefaultOrientationProvider(
updateInterval: Duration
): OrientationProvider {
TODO()
Copy link
Contributor

Choose a reason for hiding this comment

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

On iOS (compass) orientation is also provided through CLLocationManager (https://developer.apple.com/documentation/corelocation/getting-heading-and-course-information).

I think we can just make IosLocationProvider implement both LocationProvider and OrientationProvider and add enableLocation/enableOrientation constructor parameters. Alternatively, you can also duplicate the whole class.

Copy link
Contributor

@kodebach kodebach left a comment

Choose a reason for hiding this comment

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

This time for the correct commit

public fun rememberUserLocationState(
locationProvider: LocationProvider,
orientationProvider: OrientationProvider? = null,
samplePeriod: Duration? = 1.seconds,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think a better solution would be to add a

val samplePeriod: Duration

to the *Provider interfaces. The you can just use the max of those for the flow.sample call (or the code I suggest below).

But after thinking about it, I'm actually not sure what you're trying to avoid here. The trivial solution (a second collectAsStateWithLifecycle) shouldn't lead to unnecessary recompositions, assuming you want a recomposition every time either location or orientation changes. That recomposition should also just affect the composables that read location or orientation from the returned UserLocationState, i.e. normally just LocationPuck.

jgillich and others added 4 commits February 2, 2026 18:16
…e/location/LocationPuck.kt

Co-authored-by: Klemens Böswirth <23529132+kodebach@users.noreply.github.com>
Co-authored-by: Klemens Böswirth <23529132+kodebach@users.noreply.github.com>
Copy link
Contributor

@kodebach kodebach left a comment

Choose a reason for hiding this comment

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

LGTM now

@jgillich
Copy link
Collaborator Author

jgillich commented Feb 2, 2026

Actions is broken again. Dammit Github, get it together 🤦‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants