transistorsoft / react-native-background-geolocation

Sophisticated, battery-conscious background-geolocation with motion-detection
http://shop.transistorsoft.com/pages/react-native-background-geolocation
MIT License
2.65k stars 426 forks source link

Lots of jumps, spikes and outlier coordinates for both iOS and Android with high accuracy config and manually changePace(true) #1088

Closed alfredbaudisch closed 4 years ago

alfredbaudisch commented 4 years ago

Your Environment

Expected Behavior

Actual Behavior

Those images illustrate real usage from users and then the tracker goes randomly crazy (iOS, Android, many different brands and versions).

Some of the cases are very extreme and have been happening constantly the past few months: image map1 map3

More images showing spikes ![map2](https://user-images.githubusercontent.com/248383/85558293-e5e29400-b628-11ea-9d28-fdae4351d05f.png) ![image](https://user-images.githubusercontent.com/248383/85560330-d6644a80-b62a-11ea-989f-f05bc3dd951f.png)

Steps to Reproduce

  1. This is the component being added to root level of the application:
    Component
const BASE_BG_GEO_CONFIG = {
  // Geolocation Config
  // @url https://transistorsoft.github.io/react-native-background-geolocation/interfaces/_react_native_background_geolocation_.config.html
  stopOnStationary: false,
  stopTimeout: 15,
  stopOnTerminate: true,
  startOnBoot: false,

  // Aggressive settings
  pausesLocationUpdatesAutomatically: undefined,
  desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_NAVIGATION,
  distanceFilter: 0,
  locationUpdateInterval: 0,
  fastestLocationUpdateInterval: 0,
  disableElasticity: true,
  disableStopDetection: true,
  pausesLocationUpdatesAutomatically: false

  disableLocationAuthorizationAlert: !isIOS(),

  // iOS only:
  locationAuthorizationAlert: {
    titleWhenNotEnabled: "Location Services not enabled",
    titleWhenOff: "Location Services OFF",
    instructions: "You must enable \"Always\" in Location Services in order to allow Background Location Tracking",
    cancelButton: "Cancel",
    settingsButton: "Settings"
  },
  locationAuthorizationRequest: "Always",

  // Debug properties:
  debug: false,
  logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE,

  // Syncing and batching
  autoSync: true,
  batchSync: true,
  autoSyncThreshold: 10,
  maxBatchSize: 100,

  // Request Body template
  httpRootProperty: "logs",
  locationTemplate: '{"lat":<%= latitude %>,"lng":<%= longitude %>,"time":"<%= timestamp %>"}',
};

export default class ActivityTrackingComponent extends React.Component {
  constructor(props: CompleteProps) {
    super(props);

    this.state = {
      isBgGeoEnabled: false,
      isBgGeoReady: false,
      startedManually: false
    };
  }

  getBgGeoConfigWithDynamicData() {
    const config = {
      ...BASE_BG_GEO_CONFIG,

      // Upload only after the token has been retrieved
      autoSync: this.props.userToken !== null,

      url: this.props.apiHost + batchCreateActivityLogsPath,
      headers: {
        "Authorization": `Bearer ${this.props.userToken ? this.props.userToken : ""}`
      },

      extras: {
        "activity_id": this.props.activeTimerId,
        "user_id": this.props.employeeId
      },
    };

    return config;
  }

  /**
   * Set the BgGeo configuration dynamically as the timers and users changes.
   */
  reconfigureBgGeo() {
    if(this.state.isBgGeoReady) {
      BackgroundGeolocation.setConfig(this.getBgGeoConfigWithDynamicData());
    }
  }

  async configureBackgroundGeolocation() {
    const onLocation = this.onLocation.bind(this);
    const onError = this.onError.bind(this);
    BackgroundGeolocation.onLocation(onLocation, onError);

    // We can't track before calling ready
    BackgroundGeolocation.ready(this.getBgGeoConfigWithDynamicData(), (state) => {
      console.log("- BackgroundGeolocation is configured and ready: ", state.enabled);

      this.setState({
        isBgGeoEnabled: state.enabled,
        isBgGeoReady: true
      });
    });
  }

  componentDidUpdate(prevProps: CompleteProps, prevState: ActivityTrackingComponentState) {
    const isDifferentTimer = prevProps.activeTimerId !== this.props.activeTimerId;
    const isTimerActive = this.props.activeTimerId !== 0;

    if((prevProps.apiHost !== this.props.apiHost) ||
      (prevProps.userToken !== this.props.userToken) ||
      (isDifferentTimer && isTimerActive)) {
      this.reconfigureBgGeo();
    }

    this.setEnabled();
  }

  componentDidMount() {
    this.configureBackgroundGeolocation();
  }

  componentWillUnmount() {
    // this was never called in the tests, the component is kept mounted
    console.log("[ACTIVITY TRACKING UNMOUNTED]"); 
    BackgroundGeolocation.removeListeners();
  }

  onLocation(location : Object) {
    this.props.onLocation(location);
    console.log('[location] -', location);
  }

  onError(error : Object) {
    console.warn('[location] ERROR -', error);
  }

  shouldTrackingBeStarted() {
    return this.props.activeTimerId !== 0;
  }

  startBgGeolocation() {
    this.setState({startedManually: true});
    console.log("[location] START");
    BackgroundGeolocation.start().then(() => {
      if(this.isAggressivelyTracking()) {
        BackgroundGeolocation.changePace(true);
      }
    })
  }

  setEnabled() {
    const shouldBeEnabled = this.shouldTrackingBeStarted();

    if(this.state.isBgGeoReady) {
      const actualEnabled = this.state.isBgGeoEnabled;

      if((shouldBeEnabled && !this.state.startedManually) || (actualEnabled !== shouldBeEnabled)) {
        this.setState({isBgGeoEnabled: shouldBeEnabled});

        if(shouldBeEnabled) {
          this.startBgGeolocation();
        } else {
          console.log("[location] STOP");
          BackgroundGeolocation.changePace(false);
          BackgroundGeolocation.stop();
        }
      }
    }
  }

  render() {
    if(!this.state.isBgGeoReady) {
      return <ToastError text={"!isBgGeoReady"}/>;
    } else {
      const lastLocation = this.props.coordinates && this.props.lastCoordinatesAt ? this.props.lastCoordinatesAt : "no";
      return <ToastNeutral text={(`user: ${this.props.employeeId}, token: ${!!this.props.userToken}, timer: ${this.props.activeTimerId}, started: ${this.state.startedManually}`) + " - " + (this.state.isBgGeoEnabled ? "enabled" : "disabled") + " - ready: " + (this.state.isBgGeoReady ? "yes" : "no") + " - last location: " + lastLocation}/>;
    }
  }
}

  1. Notice that I'm calling changePace(true)
    BackgroundGeolocation.start().then(() => {
    BackgroundGeolocation.changePace(true);
    })

    and

BackgroundGeolocation.changePace(false);
BackgroundGeolocation.stop();
  1. Start moving, at various speeds, but most of the time 5-10km/h.

Context

Debug logs

The log emailed from iOS with emailLog is 70k lines long, so attaching it here instead of pasting: background-geolocation.log.gz

This log is related to the whole tracking of the 2nd attached image (Activity Map)

christocracy commented 4 years ago

This is not a plugin issue. More likely environment / device related.

The plugin does not manufacture locations, it receives them from the device’s native location API in the same manner as any native app.

alfredbaudisch commented 4 years ago

But how come it is happening accross different devices, OSs and users? It can't be device related. Not only one, but all of our users are getting this since March, no exception. We got reports from: Android (Huawei, Samsung, OnePlus) and every possible different iPhone.

  1. I wonder whether it is something in our configuration and usage of the library, that it is making the GPS lose accuracy and capture those crazy jumps?
  2. Do you have any recommendation on how to take care of this? What we could tweak / test in the library?
christocracy commented 4 years ago

I suggest you install the SampleApp, linked in the README. Don’t look at the code, just run it on your device(s).

It will upload locations to tracker.transistorsoft.com.

christocracy commented 4 years ago

There is no correlation between iOS vs Android. They are completely different. One is written in Obj-c, the other in Java.

I do not experience this on my 12 test devices. I have several years of logs.

christocracy commented 4 years ago

Are you observing the accuracy of these “spikes”. You’ll likely find they’re >= 1000, meaning the location came from a cell tower, where gps or wifi location was not available from the device’s location-services.

alfredbaudisch commented 4 years ago

There is no correlation between iOS vs Android. They are completely different. One is written in Obj-c, the other in Java.

Still, all of our users got the spikes, both Android and iOS. So by pointing out this, I'm pointing that it seems that we are doing something wrong on the configuration side or even in how we implement the library in React Native.

That's why I was asking for your help or pointers, since you know the library and config from inside out and probably saw every possible scenario related to it.

Are you observing the accuracy of these “spikes”. You’ll likely find they’re >= 1000, meaning the location came from a cell tower, where gps or wifi location was not available from the device’s location-services.

Even in situations like this one from the image, where it shoots all over the place?

image

Sometimes they are <100 meters, like those ones: image

I'm implementing a backend filter for those outliers, the problem is because they are very random and when they are close to the actual location, I may end up deleting the desired location itself. It would be good if this data wasn't coming from the tracking itself.

I suggest you install the SampleApp, linked in the README. Don’t look at the code, just run it on your device(s).

I tested and used the SampleApp when implementing the project and learned a lot with it and got some ideas from it. I know it works, I never questioned it. And I don't think looking at it again will help us, because we already have something running with a lot of production data.

dorthwein commented 4 years ago

We see this as well and have been struggling with how to deal with it properly for a long time (multiple years). We was our data through an OSRM routing server to clean up tracking to actually have a recording that closely snaps to a route as a result.

We get user reports of this as well when using navigation functionality prompting a re-route. We also set pace manually etc...

alfredbaudisch commented 4 years ago

We see this as well and have been struggling with how to deal with it properly for a long time (multiple years). We was our data through an OSRM routing server to clean up tracking to actually have a recording that closely snaps to a route as a result.

We get user reports of this as well when using navigation functionality prompting a re-route. We also set pace manually etc...

Thank you for your input. We can't use an OSRM routing server because most of our tracking happens in farming fields, but what I'm implementing is basically an outlier detector. If the next coordinate is >= THRESHOLD than the previous one, it is going to be deleted.

It will not solve all cases, because we have A LOT of cases like "small triangles": image

But it will help solving the extreme cases.

alfredbaudisch commented 4 years ago

Whoever comes across this issue, the attached PDF quickly describes the result of my experimentation and which solutions I implemented. The solutions are not perfect, but they are simple and straightforward. None of the solutions is related to this library or React Native or geolocation itself.

Basically, the noises will still be captured and they interfere with geofencing greatly, but after a path is finished being captured, it's possible to mask/hide most of the noises.

Tracking path jumps and spikes removal experimentation.pdf

  1. Remove vertices too far apart manually
  2. Simplify path with either PostgreSQL PostGIS Simplify or Simplify.js
  3. Smooth path with PostGIS

Just be aware that since the solutions are not related to the library, ridiculous "paths" like this will still be registered by the library: image