Background geolocation in Ionic app

This blog show an example on how to implement background geolocation in an Ionic app using the background geolocation plugin.  to capture user locations for building location aware applications

Install plugin

First add the cordova plugin to the Ionic app:

cordova plugin add cordova-plugin-mauron85-background-geolocation

Implement background geolocation service

Next, implement an Angular service:

var app = angular.module('starter');
app.factory('BackgroundGeolocationService', ['$q', '$http', function ($q, $http) { 
      var callbackFn = function(location) {
          postLocation(location);
          backgroundGeoLocation.finish();  
      },
      failureFn = function(error) {
          console.log('BackgroundGeoLocation error ' + JSON.stringify(error));  
      },

      //Enable background geolocation  
      start = function () {      
          //save settings (background tracking is enabled) in local storage    
          window.localStorage.setItem('bgGPS', 1);
          backgroundGeoLocation.configure(callbackFn, failureFn, {
               desiredAccuracy: 10,      
               stationaryRadius: 10,
               distanceFilter: 10,
               locationProvider: 'ANDROID_ACTIVITY_PROVIDER',
               interval: 10000,      
               fastestInterval: 5000,      
               stopOnStillActivity: false,
               debug: false,      
               stopOnTerminate: false
         });
         backgroundGeoLocation.start();
      };
     // Send location to a backend server, e.g. for location tracking
     postLocation = function post(location) {
         return $http(      
           {        
              method: 'POST',
              headers: {
                   "Content-Type": "application/json"
                    },
              url: 'http://<location server DNS>/location',
              data: {          
                    lat : location.latitude,
                    lng : location.longitude        
                    }      
           }).then(function (response) {
                   return response.data;      
           });  
       }
  
       return {    
            start: start,
            // Stop data tracking    
            stop: function () {
                window.localStorage.setItem('bgGPS', 0);
                backgroundGeoLocation.stop();    
            }  
      }
}]);

The codes above are largely based on this blog article.  I have modified the plugin configuration (see notes below). Details about each configuration parameter can be found in the plugin documentation. I am setting it up and tested for Android here. A few notes or tips below:

  1. Two location providers are supported in Android – Android_distance_filter_provider and Android_activity_provider. I end up using the 2nd one to adjust the intervals in which the app gets location update by setting the parameters interval and fastinterval (to 10 and 5 seconds respectively).
  2. Debug is your friend. Set this to true will trigger a sound and notification when the app receives a location update.
  3. I use a combination of parameters desiredAccuracy, stationaryRadius and distanceFilter to define how often the app should receive location update events. The values set here seems to strike a good balance between accuracy/frequency and battery usage but its largely depends on the app’s use cases and phones.
  4. The plugin includes a url paramter where you can set to the backend server to post the locations. It is also possible, and more flexible, to just implement your own method and call it in the callback function callbackFn 

To kick start the plugin, call init() method in the app.js :

angular.module('app', ['ionic', ...])
.run(function($ionicPlatform, $http, BackgroundGeolocationService) {
   $ionicPlatform.ready(function() {    
       if(window.StatusBar) {
          StatusBar.styleDefault();
       }
       BackgroundGeolocationService.init();
   });
});

Now the Ionic app can send geolocations events when it is in the background, it is rather straight forward to implement a backend server to track and display users’ current locations. Below is a screenshot of a Java app I build, for example:

blog_track.png

 

Advertisements

Using Google Maps Directions APIs with more than 8 waypoints

Google Maps Directions APIs are great to create routes and directions for use in many practical business problems, e.g. delivery routing. One limitation is it only allows 8 (or 23 in premium plan) waypoints between the origin and destination locations. This blog demonstrates how to work around this limitation with the Maps JavaScript API. I am implementing the solution with TypeScript for use in an AngularJS 1.x application.

The idea here is straight forward: Given a route with more than 8 waypoints, i.e. 10 or more locations, we divide the route into multiple legs where each leg can have at most 8 waypoints. That is for a route with locations

[1,2,3,...,n] where n > 10

we would like to end up with multiple legs like:

leg 1: [1,2,3,...,10]
leg 2: [10,11,...,19]
...
leg 3: [...,n]

then we can use the directions API to get the route for each leg and concatenate the results back together as a single route on Google Maps.

First let define the objects to represent the route, waypoints and legs.

// Class to represent a route
export class Route {
 totalDistanceInKm : number;
 totalDurationInMins : number;
 drops : Drop[];
 legs : Leg[];
...
}

// Class to represent a drop (or waypoint)
export class Drop {
 location : google.maps.LatLng;
 // route details from directions service
 distanceInKm : number; // route distance from previous drop
 durationInMin : number; // route duration from previous drop
...
}

// Class to represent a leg of a route, used as input to directions service
export class Leg {
 origin : google.maps.LatLng;
 destination : google.maps.LatLng;
 waypoints : Array<google.maps.LatLng>;
}

A route can have multiple drops. The first and last drop corresponds to route origin and destination respectively. A drop consists of just a latlng location and a number of variables to store results, e.g. distance and duration, from the directions service response. The Leg class stores the information required as input requests to the directions service.

To create a route from a list of locations, use the following methods (assuming the class member _currentRoute contains the list of drops of the input route.

export class DeliveryRouteService implements IDeliveryRouteService {

...

 private createLegs() : void {
      var route = this._currentRoute;
      var drops = route.drops;
      route.legs = [];
      var i = 0; 
      var j = Math.min(drops.length, 10);

      while (j <= drops.length && j > i + 2) {
           route.legs.push(this.createLeg( drops.slice(i,j))); 
           i = j - 1;
           j = Math.min(drops.length, i + 10);
      }
 }

 private createLeg(drops : Drop[]) : Leg {
      var leg = new Leg();
      leg.origin = drops[0].location;
      leg.destination = drops[drops.length-1].location;
      leg.waypoints = [];
      if (drops.length > 2) {
           for (var k = 1; k < drops.length - 1; k++) {
                leg.waypoints.push(drops[k].location);
           }
      }
      return leg;
 }

With the route and legs created, you could call the directions API to obtain the route one leg at a time and concatenate the results for the whole route in your own codes. The following code snippets demonstrate how this could be done:

public directionsForLeg(idx : number, dropIdx : number) {
      var timeout : ng.ITimeoutService = this._timeoutService;
      var leg = this._currentRoute.legs[idx];
      var waypoints : google.maps.DirectionsWaypoint[] = [];
      
      for (var i = 0; i < leg.waypoints.length; i++) {
           waypoints.push(<google.maps.DirectionsWaypoint> {
           location : leg.waypoints[i],
           stopover : true
          })
      }
      // (1) Create directions API request
      var request = <google.maps.DirectionsRequest> {
           origin : leg.origin,
           destination : leg.destination,
           waypoints : waypoints,
           optimizeWaypoints : false,
           travelMode : google.maps.TravelMode.DRIVING
      }
 
      // (2) invoke directions service using wrapper service
      this._googleMapService.directions(request).then(function(result) {
           var resultLegs = result.routes[0].legs;
           for (var j = 0; j < resultLegs.length; j++) {
             // do something with results here, e.g. get leg distance/duration
             ...
             dropIdx++;
           }

           if (idx < that._currentRoute.legs.length - 1) {
                // (3) do the next leg if there are more, 
                // put some delay to avoid hitting request limit
                timeout(function() {
                 that.directionsForLeg(idx+1, dropIdx);
                }, 1000, true);
           } else {
                // all done, notify route/directions is ready
                ...
           }
      }, function(e) {
           that._logger.error('directions service failed: ' + e);
      });
 }

Note the recursion used to call directions service for the next leg in (3). To get route and directions for the entire route,  just call

 directionsForLeg(0, 1); // start with 1, first drop is origin

Below is the wrapper service for Google Maps JavaScript API

export class GoogleMapsService {
 static id : string = 'GoogleMapsService';
 private _map : google.maps.Map;
 private _renderers : google.maps.DirectionsRenderer[];
 private _directionsService : google.maps.DirectionsService;

...

      directions(request : google.maps.DirectionsRequest) : ng.IPromise<any> {
           var deferred = this.$q.defer();
           // create renderer
           var opts = <google.maps.DirectionsRendererOptions> {
               map : this._map,
               suppressMarkers : true
           };
           var renderer = new google.maps.DirectionsRenderer(opts);
           this._renderers.push(renderer);

           this._directionsService.route(request, function(response, status) {
               if (status == google.maps.DirectionsStatus.OK) {
                    renderer.setDirections(response);
                    deferred.resolve(response);
               } else {
                    deferred.reject(status.toString());
               }
           })
           return deferred.promise;
      }

}

Each call to the directions service will create and render the route for the input leg on the map.

The work around here has one obvious limitation – you can’t optimize the waypoints for the entire route. In practice, this may not be a severe issue if the waypoints are reasonably well ordered. It is still possible to apply optimization to each leg to improve the overall route distance and duration.