Skip to content

Fix issue #1574: Add location accuracy filter to prevent GPS drift #1702

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions geolocator_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## UNRELEASED

* Adds location accuracy filtering to prevent random GPS drifts (Issue #1574). Features added:
* `LocationAccuracyFilter` class to filter out location updates with unrealistic movement patterns
* Optional filtering through new `enableAccuracyFilter` parameter
* Configurable thresholds for accuracy, speed, and distance jumps
* Comprehensive test coverage for various transportation modes

## 5.0.1+1

- Bump `androidx.core:core` to version 1.16.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ public synchronized void onLocationResult(@NonNull LocationResult locationResult
}

nmeaClient.enrichExtrasWithNmea(location);
positionChangedCallback.onPositionChanged(location);

// Apply the location accuracy filter before reporting the position
if (LocationAccuracyFilter.shouldAcceptLocation(location)) {
positionChangedCallback.onPositionChanged(location);
}
}

@Override
Expand Down Expand Up @@ -230,6 +234,13 @@ public void startPositionUpdates(
this.positionChangedCallback = positionChangedCallback;
this.errorCallback = errorCallback;

// Enable or disable the location accuracy filter based on options
if (this.locationOptions != null) {
LocationAccuracyFilter.setFilterEnabled(this.locationOptions.isEnableAccuracyFilter());
} else {
LocationAccuracyFilter.setFilterEnabled(false);
}

LocationRequest locationRequest = buildLocationRequest(this.locationOptions);
LocationSettingsRequest settingsRequest = buildLocationSettingsRequest(locationRequest);

Expand Down Expand Up @@ -278,5 +289,7 @@ public void startPositionUpdates(
public void stopPositionUpdates() {
this.nmeaClient.stop();
fusedLocationProviderClient.removeLocationUpdates(locationCallback);
// Reset the location filter when updates are stopped
LocationAccuracyFilter.reset();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.baseflow.geolocator.location;

import android.location.Location;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
* Utility class to filter inaccurate location updates that might cause GPS drift.
* Detects and filters out positions that are physically implausible based on
* speed, distance, and accuracy.
*/
public class LocationAccuracyFilter {
private static final String TAG = "LocationAccuracyFilter";

// Constants for filtering
private static final float MAX_ACCURACY_THRESHOLD = 300.0f; // meters
private static final float MAX_SPEED_THRESHOLD = 280.0f; // m/s (~1000 km/h to support aircraft)
private static final float MAX_DISTANCE_JUMP = 1000.0f; // meters

@Nullable private static Location lastFilteredLocation = null;
private static boolean filterEnabled = false;

/**
* Sets whether filtering is enabled.
* This should be called when starting location updates based on user preferences.
*
* @param enabled Whether filtering should be enabled
*/
public static void setFilterEnabled(boolean enabled) {
filterEnabled = enabled;
// Reset the filter state when changing the setting
if (enabled) {
reset();
}
}

/**
* Checks if a location update should be accepted based on realistic movement patterns.
*
* @param newLocation The new location to evaluate
* @return true if the location should be accepted, false if it should be filtered
*/
public static boolean shouldAcceptLocation(@NonNull Location newLocation) {
// If filtering is disabled, always accept locations
if (!filterEnabled) {
return true;
}

// Always accept the first position
if (lastFilteredLocation == null) {
lastFilteredLocation = newLocation;
return true;
}

// Time difference in seconds
float timeDelta = (newLocation.getTime() - lastFilteredLocation.getTime()) / 1000.0f;

// Don't filter if time hasn't advanced
if (timeDelta <= 0) {
lastFilteredLocation = newLocation;
return true;
}

// Calculate distance in meters
float distance = newLocation.distanceTo(lastFilteredLocation);

// Calculate speed (m/s)
float speed = distance / timeDelta;

// Get position accuracy (if available)
float accuracy = newLocation.hasAccuracy() ? newLocation.getAccuracy() : 0.0f;

// Filters to apply - use a conservative approach to avoid affecting legitimate use cases
boolean shouldFilter = false;

// Filter based on very poor accuracy - this catches points with extremely poor accuracy
if (accuracy > MAX_ACCURACY_THRESHOLD) {
Log.d(TAG, "Filtered location: Poor accuracy " + accuracy + "m");
shouldFilter = true;
}

// Filter based on unrealistically high speeds
if (speed > MAX_SPEED_THRESHOLD) {
Log.d(TAG, "Filtered location: Unrealistic speed " + speed + "m/s");
shouldFilter = true;
}

// Filter based on large jumps when accuracy is moderate or poor
if (distance > MAX_DISTANCE_JUMP && accuracy > 75.0f) {
Log.d(TAG, "Filtered location: Large jump with poor accuracy - distance: "
+ distance + "m, accuracy: " + accuracy + "m");
shouldFilter = true;
}

// Accept and update reference if it passes filters
if (!shouldFilter) {
lastFilteredLocation = newLocation;
}

return !shouldFilter;
}

/**
* Resets the internal state of the filter.
* This should be called when location updates are stopped.
*/
public static void reset() {
lastFilteredLocation = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ public void startPositionUpdates(
? LocationRequestCompat.PASSIVE_INTERVAL
: locationOptions.getTimeInterval();
quality = accuracyToQuality(accuracy);

// Enable or disable the location accuracy filter based on options
LocationAccuracyFilter.setFilterEnabled(locationOptions.isEnableAccuracyFilter());
} else {
LocationAccuracyFilter.setFilterEnabled(false);
}

this.currentLocationProvider = determineProvider(this.locationManager, accuracy);
Expand Down Expand Up @@ -202,11 +207,12 @@ public void stopPositionUpdates() {
this.isListening = false;
this.nmeaClient.stop();
this.locationManager.removeUpdates(this);
LocationAccuracyFilter.reset();
}

@Override
public synchronized void onLocationChanged(Location location) {
if (isBetterLocation(location, currentBestLocation)) {
if (isBetterLocation(location, currentBestLocation) && LocationAccuracyFilter.shouldAcceptLocation(location)) {
this.currentBestLocation = location;

if (this.positionChangedCallback != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,27 @@ public class LocationOptions {
private final long distanceFilter;
private final long timeInterval;
private final boolean useMSLAltitude;
private final boolean enableAccuracyFilter;

private LocationOptions(
LocationAccuracy accuracy, long distanceFilter, long timeInterval, boolean useMSLAltitude) {
LocationAccuracy accuracy, long distanceFilter, long timeInterval, boolean useMSLAltitude, boolean enableAccuracyFilter) {
this.accuracy = accuracy;
this.distanceFilter = distanceFilter;
this.timeInterval = timeInterval;
this.useMSLAltitude = useMSLAltitude;
this.enableAccuracyFilter = enableAccuracyFilter;
}

public static LocationOptions parseArguments(Map<String, Object> arguments) {
if (arguments == null) {
return new LocationOptions(LocationAccuracy.best, 0, 5000, false);
return new LocationOptions(LocationAccuracy.best, 0, 5000, false, false);
}

final Integer accuracy = (Integer) arguments.get("accuracy");
final Integer distanceFilter = (Integer) arguments.get("distanceFilter");
final Integer timeInterval = (Integer) arguments.get("timeInterval");
final Boolean useMSLAltitude = (Boolean) arguments.get("useMSLAltitude");
final Boolean enableAccuracyFilter = (Boolean) arguments.get("enableAccuracyFilter");

LocationAccuracy locationAccuracy = LocationAccuracy.best;

Expand Down Expand Up @@ -57,7 +60,8 @@ public static LocationOptions parseArguments(Map<String, Object> arguments) {
locationAccuracy,
distanceFilter != null ? distanceFilter : 0,
timeInterval != null ? timeInterval : 5000,
useMSLAltitude != null && useMSLAltitude);
useMSLAltitude != null && useMSLAltitude,
enableAccuracyFilter != null && enableAccuracyFilter);
}

public LocationAccuracy getAccuracy() {
Expand All @@ -75,4 +79,8 @@ public long getTimeInterval() {
public boolean isUseMSLAltitude() {
return useMSLAltitude;
}

public boolean isEnableAccuracyFilter() {
return enableAccuracyFilter;
}
}
Loading