From 0745f2feea8872e0ec1967c8ab87950ce59233f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 13 Oct 2026 04:02:35 +0200 Subject: [PATCH] fix(android): report compass heading in location updates Android filled both heading and course from the GPS bearing, so stationary fixes had no heading and moving ones reported the course as heading. Send course from the location bearing and heading from the Mapbox location plugin's compass (DefaultLocationProvider with PuckBearing.HEADING), only while location updates are subscribed. Fixes #4063 --- .../rnmapbox/rnmbx/events/LocationEvent.kt | 14 +++-- .../rnmbx/modules/RNMBXLocationModule.kt | 59 ++++++++++++++++++- src/modules/location/locationManager.ts | 2 - 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/events/LocationEvent.kt b/android/src/main/java/com/rnmapbox/rnmbx/events/LocationEvent.kt index d6c44899d0..ebe03103ad 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/events/LocationEvent.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/events/LocationEvent.kt @@ -10,14 +10,18 @@ import java.util.* import com.rnmapbox.rnmbx.v11compat.location.* -class LocationEvent(private val location: Location, private val mapView: RNMBXMapView?) : IEvent { +class LocationEvent( + private val location: Location, + private val mapView: RNMBXMapView?, + private val heading: Double? = null +) : IEvent { val uUID: UUID init { uUID = UUID.randomUUID() } - constructor(location: Location) : this(location, null) {} + constructor(location: Location, heading: Double? = null) : this(location, null, heading) override val iD get() = mapView?.id ?: -1 @@ -50,12 +54,12 @@ class LocationEvent(private val location: Location, private val mapView: RNMBXMa coords.putDouble("altitude", it) } coords.putDouble("accuracy", location.accuracy.toDouble()) - // A better solution will be to pull the heading from the compass engine, - // unfortunately the api is not publicly available in the mapbox sdk location.bearing?.let { - coords.putDouble("heading", it.toDouble()) coords.putDouble("course", it.toDouble()) } + heading?.let { + coords.putDouble("heading", it) + } location.speed?.let { coords.putDouble("speed", it.toDouble()) } diff --git a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXLocationModule.kt b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXLocationModule.kt index 6af153e6b6..f8c8202a02 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXLocationModule.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXLocationModule.kt @@ -1,5 +1,6 @@ package com.rnmapbox.rnmbx.modules +import android.animation.ValueAnimator import com.facebook.react.bridge.* import com.rnmapbox.rnmbx.location.LocationManager import com.facebook.react.module.annotations.ReactModule @@ -7,7 +8,12 @@ import com.rnmapbox.rnmbx.NativeRNMBXLocationModuleSpec import com.rnmapbox.rnmbx.location.LocationManager.OnUserLocationChange import com.rnmapbox.rnmbx.events.LocationEvent import com.rnmapbox.rnmbx.location.LocationManager.Companion.getInstance +import com.mapbox.common.location.LocationError +import com.mapbox.geojson.Point +import com.mapbox.maps.plugin.locationcomponent.DefaultLocationProvider +import com.mapbox.maps.plugin.locationcomponent.LocationConsumer import java.lang.Exception +import kotlin.math.abs import com.rnmapbox.rnmbx.v11compat.location.* @@ -22,16 +28,21 @@ class RNMBXLocationModule(reactContext: ReactApplicationContext) : private val locationManager: LocationManager? = getInstance(reactContext) private var mLastLocation: Location? = null private var locationEventThrottle: LocationEventThrottle = LocationEventThrottle() + private var mLastHeading: Double? = null + private var mLastSentHeading: Double? = null + private var headingProvider: DefaultLocationProvider? = null private val lifecycleEventListener: LifecycleEventListener = object : LifecycleEventListener { override fun onHostResume() { if (isEnabled) { locationManager?.resume() + startHeadingUpdates() } } override fun onHostPause() { locationManager?.pause() + stopHeadingUpdates() } override fun onHostDestroy() { @@ -56,12 +67,50 @@ class RNMBXLocationModule(reactContext: ReactApplicationContext) : } mLastLocation = location if (changed && (location != null) && shouldSendLocationEvent()) { - val locationEvent = LocationEvent(location) - emitOnLocationUpdate(locationEvent.toJSON()) + sendLocationEvent(location) } } } + private val headingConsumer = object : LocationConsumer { + override fun onBearingUpdated(vararg bearing: Double, options: (ValueAnimator.() -> Unit)?) { + val heading = bearing.lastOrNull()?.let { (it % 360.0 + 360.0) % 360.0 } ?: return + mLastHeading = heading + val location = mLastLocation ?: return + // Like CLLocationManager's default headingFilter, ignore changes under 1 degree + val lastSent = mLastSentHeading + if (lastSent != null && abs((heading - lastSent + 540.0) % 360.0 - 180.0) < 1.0) return + if (shouldSendLocationEvent()) { + sendLocationEvent(location) + } + } + + override fun onLocationUpdated(vararg location: Point, options: (ValueAnimator.() -> Unit)?) {} + override fun onPuckLocationAnimatorDefaultOptionsUpdated(options: ValueAnimator.() -> Unit) {} + override fun onPuckBearingAnimatorDefaultOptionsUpdated(options: ValueAnimator.() -> Unit) {} + override fun onHorizontalAccuracyRadiusUpdated(vararg radius: Double, options: (ValueAnimator.() -> Unit)?) {} + override fun onPuckAccuracyRadiusAnimatorDefaultOptionsUpdated(options: ValueAnimator.() -> Unit) {} + override fun onError(error: LocationError) {} + } + + private fun sendLocationEvent(location: Location) { + mLastSentHeading = mLastHeading + emitOnLocationUpdate(LocationEvent(location, mLastHeading).toJSON()) + } + + private fun startHeadingUpdates() { + if (headingProvider != null) return + headingProvider = DefaultLocationProvider(reactApplicationContext).also { + it.updatePuckBearing(PuckBearing.HEADING) + it.registerLocationConsumer(headingConsumer) + } + } + + private fun stopHeadingUpdates() { + headingProvider?.unRegisterLocationConsumer(headingConsumer) + headingProvider = null + } + init { reactContext.addLifecycleEventListener(lifecycleEventListener) } @@ -107,7 +156,7 @@ class RNMBXLocationModule(reactContext: ReactApplicationContext) : override fun onSuccess(result: LocationEngineResult) { val location = result.lastLocation if (location != null) { - val locationEvent = LocationEvent(location) + val locationEvent = LocationEvent(location, mLastHeading) promise.resolve(locationEvent.payload) } else { promise.resolve(null) @@ -140,6 +189,7 @@ class RNMBXLocationModule(reactContext: ReactApplicationContext) : locationManager!!.addLocationListener(onUserLocationChangeCallback) locationManager.setMinDisplacement(mMinDisplacement) locationManager.startCounted() + startHeadingUpdates() } private fun stopLocationManager() { @@ -148,8 +198,11 @@ class RNMBXLocationModule(reactContext: ReactApplicationContext) : } locationManager!!.removeLocationListener(onUserLocationChangeCallback) locationManager.stopCounted() + stopHeadingUpdates() isEnabled = false mLastLocation = null + mLastHeading = null + mLastSentHeading = null } // region Location event throttle diff --git a/src/modules/location/locationManager.ts b/src/modules/location/locationManager.ts index 303fca8a94..6c2e4402b9 100644 --- a/src/modules/location/locationManager.ts +++ b/src/modules/location/locationManager.ts @@ -42,8 +42,6 @@ interface Coordinates { /** * The heading (measured in degrees) relative to true north. * Heading is used to describe the direction the device is pointing to (the value of the compass). - * Note that on Android this is incorrectly reporting the course value as mentioned in issue https://github.com/rnmapbox/maps/issues/1213 - * and will be corrected in a future update. */ heading?: number;