Skip to content

Commit 57a6be5

Browse files
committed
Fix issue #1574: Add location accuracy filter to prevent GPS drift
1 parent 708f0ab commit 57a6be5

File tree

6 files changed

+356
-5
lines changed

6 files changed

+356
-5
lines changed

geolocator_android/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## UNRELEASED
2+
3+
* Adds location accuracy filtering to prevent random GPS drifts (Issue #1574). Features added:
4+
* `LocationAccuracyFilter` class to filter out location updates with unrealistic movement patterns
5+
* Optional filtering through new `enableAccuracyFilter` parameter
6+
* Configurable thresholds for accuracy, speed, and distance jumps
7+
* Comprehensive test coverage for various transportation modes
8+
19
## 5.0.1+1
210

311
- Bump `androidx.core:core` to version 1.16.0

geolocator_android/android/src/main/java/com/baseflow/geolocator/location/FusedLocationClient.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ public synchronized void onLocationResult(@NonNull LocationResult locationResult
7979
}
8080

8181
nmeaClient.enrichExtrasWithNmea(location);
82-
positionChangedCallback.onPositionChanged(location);
82+
83+
// Apply the location accuracy filter before reporting the position
84+
if (LocationAccuracyFilter.shouldAcceptLocation(location)) {
85+
positionChangedCallback.onPositionChanged(location);
86+
}
8387
}
8488

8589
@Override
@@ -230,6 +234,13 @@ public void startPositionUpdates(
230234
this.positionChangedCallback = positionChangedCallback;
231235
this.errorCallback = errorCallback;
232236

237+
// Enable or disable the location accuracy filter based on options
238+
if (this.locationOptions != null) {
239+
LocationAccuracyFilter.setFilterEnabled(this.locationOptions.isEnableAccuracyFilter());
240+
} else {
241+
LocationAccuracyFilter.setFilterEnabled(false);
242+
}
243+
233244
LocationRequest locationRequest = buildLocationRequest(this.locationOptions);
234245
LocationSettingsRequest settingsRequest = buildLocationSettingsRequest(locationRequest);
235246

@@ -278,5 +289,7 @@ public void startPositionUpdates(
278289
public void stopPositionUpdates() {
279290
this.nmeaClient.stop();
280291
fusedLocationProviderClient.removeLocationUpdates(locationCallback);
292+
// Reset the location filter when updates are stopped
293+
LocationAccuracyFilter.reset();
281294
}
282295
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.baseflow.geolocator.location;
2+
3+
import android.location.Location;
4+
import android.util.Log;
5+
6+
import androidx.annotation.NonNull;
7+
import androidx.annotation.Nullable;
8+
9+
/**
10+
* Utility class to filter inaccurate location updates that might cause GPS drift.
11+
* Detects and filters out positions that are physically implausible based on
12+
* speed, distance, and accuracy.
13+
*/
14+
public class LocationAccuracyFilter {
15+
private static final String TAG = "LocationAccuracyFilter";
16+
17+
// Constants for filtering
18+
private static final float MAX_ACCURACY_THRESHOLD = 300.0f; // meters
19+
private static final float MAX_SPEED_THRESHOLD = 280.0f; // m/s (~1000 km/h to support aircraft)
20+
private static final float MAX_DISTANCE_JUMP = 1000.0f; // meters
21+
22+
@Nullable private static Location lastFilteredLocation = null;
23+
private static boolean filterEnabled = false;
24+
25+
/**
26+
* Sets whether filtering is enabled.
27+
* This should be called when starting location updates based on user preferences.
28+
*
29+
* @param enabled Whether filtering should be enabled
30+
*/
31+
public static void setFilterEnabled(boolean enabled) {
32+
filterEnabled = enabled;
33+
// Reset the filter state when changing the setting
34+
if (enabled) {
35+
reset();
36+
}
37+
}
38+
39+
/**
40+
* Checks if a location update should be accepted based on realistic movement patterns.
41+
*
42+
* @param newLocation The new location to evaluate
43+
* @return true if the location should be accepted, false if it should be filtered
44+
*/
45+
public static boolean shouldAcceptLocation(@NonNull Location newLocation) {
46+
// If filtering is disabled, always accept locations
47+
if (!filterEnabled) {
48+
return true;
49+
}
50+
51+
// Always accept the first position
52+
if (lastFilteredLocation == null) {
53+
lastFilteredLocation = newLocation;
54+
return true;
55+
}
56+
57+
// Time difference in seconds
58+
float timeDelta = (newLocation.getTime() - lastFilteredLocation.getTime()) / 1000.0f;
59+
60+
// Don't filter if time hasn't advanced
61+
if (timeDelta <= 0) {
62+
lastFilteredLocation = newLocation;
63+
return true;
64+
}
65+
66+
// Calculate distance in meters
67+
float distance = newLocation.distanceTo(lastFilteredLocation);
68+
69+
// Calculate speed (m/s)
70+
float speed = distance / timeDelta;
71+
72+
// Get position accuracy (if available)
73+
float accuracy = newLocation.hasAccuracy() ? newLocation.getAccuracy() : 0.0f;
74+
75+
// Filters to apply - use a conservative approach to avoid affecting legitimate use cases
76+
boolean shouldFilter = false;
77+
78+
// Filter based on very poor accuracy - this catches points with extremely poor accuracy
79+
if (accuracy > MAX_ACCURACY_THRESHOLD) {
80+
Log.d(TAG, "Filtered location: Poor accuracy " + accuracy + "m");
81+
shouldFilter = true;
82+
}
83+
84+
// Filter based on unrealistically high speeds
85+
if (speed > MAX_SPEED_THRESHOLD) {
86+
Log.d(TAG, "Filtered location: Unrealistic speed " + speed + "m/s");
87+
shouldFilter = true;
88+
}
89+
90+
// Filter based on large jumps when accuracy is moderate or poor
91+
if (distance > MAX_DISTANCE_JUMP && accuracy > 75.0f) {
92+
Log.d(TAG, "Filtered location: Large jump with poor accuracy - distance: "
93+
+ distance + "m, accuracy: " + accuracy + "m");
94+
shouldFilter = true;
95+
}
96+
97+
// Accept and update reference if it passes filters
98+
if (!shouldFilter) {
99+
lastFilteredLocation = newLocation;
100+
}
101+
102+
return !shouldFilter;
103+
}
104+
105+
/**
106+
* Resets the internal state of the filter.
107+
* This should be called when location updates are stopped.
108+
*/
109+
public static void reset() {
110+
lastFilteredLocation = null;
111+
}
112+
}

geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationManagerClient.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ public void startPositionUpdates(
170170
? LocationRequestCompat.PASSIVE_INTERVAL
171171
: locationOptions.getTimeInterval();
172172
quality = accuracyToQuality(accuracy);
173+
174+
// Enable or disable the location accuracy filter based on options
175+
LocationAccuracyFilter.setFilterEnabled(locationOptions.isEnableAccuracyFilter());
176+
} else {
177+
LocationAccuracyFilter.setFilterEnabled(false);
173178
}
174179

175180
this.currentLocationProvider = determineProvider(this.locationManager, accuracy);
@@ -202,11 +207,12 @@ public void stopPositionUpdates() {
202207
this.isListening = false;
203208
this.nmeaClient.stop();
204209
this.locationManager.removeUpdates(this);
210+
LocationAccuracyFilter.reset();
205211
}
206212

207213
@Override
208214
public synchronized void onLocationChanged(Location location) {
209-
if (isBetterLocation(location, currentBestLocation)) {
215+
if (isBetterLocation(location, currentBestLocation) && LocationAccuracyFilter.shouldAcceptLocation(location)) {
210216
this.currentBestLocation = location;
211217

212218
if (this.positionChangedCallback != null) {

geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationOptions.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,27 @@ public class LocationOptions {
99
private final long distanceFilter;
1010
private final long timeInterval;
1111
private final boolean useMSLAltitude;
12+
private final boolean enableAccuracyFilter;
1213

1314
private LocationOptions(
14-
LocationAccuracy accuracy, long distanceFilter, long timeInterval, boolean useMSLAltitude) {
15+
LocationAccuracy accuracy, long distanceFilter, long timeInterval, boolean useMSLAltitude, boolean enableAccuracyFilter) {
1516
this.accuracy = accuracy;
1617
this.distanceFilter = distanceFilter;
1718
this.timeInterval = timeInterval;
1819
this.useMSLAltitude = useMSLAltitude;
20+
this.enableAccuracyFilter = enableAccuracyFilter;
1921
}
2022

2123
public static LocationOptions parseArguments(Map<String, Object> arguments) {
2224
if (arguments == null) {
23-
return new LocationOptions(LocationAccuracy.best, 0, 5000, false);
25+
return new LocationOptions(LocationAccuracy.best, 0, 5000, false, false);
2426
}
2527

2628
final Integer accuracy = (Integer) arguments.get("accuracy");
2729
final Integer distanceFilter = (Integer) arguments.get("distanceFilter");
2830
final Integer timeInterval = (Integer) arguments.get("timeInterval");
2931
final Boolean useMSLAltitude = (Boolean) arguments.get("useMSLAltitude");
32+
final Boolean enableAccuracyFilter = (Boolean) arguments.get("enableAccuracyFilter");
3033

3134
LocationAccuracy locationAccuracy = LocationAccuracy.best;
3235

@@ -57,7 +60,8 @@ public static LocationOptions parseArguments(Map<String, Object> arguments) {
5760
locationAccuracy,
5861
distanceFilter != null ? distanceFilter : 0,
5962
timeInterval != null ? timeInterval : 5000,
60-
useMSLAltitude != null && useMSLAltitude);
63+
useMSLAltitude != null && useMSLAltitude,
64+
enableAccuracyFilter != null && enableAccuracyFilter);
6165
}
6266

6367
public LocationAccuracy getAccuracy() {
@@ -75,4 +79,8 @@ public long getTimeInterval() {
7579
public boolean isUseMSLAltitude() {
7680
return useMSLAltitude;
7781
}
82+
83+
public boolean isEnableAccuracyFilter() {
84+
return enableAccuracyFilter;
85+
}
7886
}

0 commit comments

Comments
 (0)