How To Enhance Location Aware Apps with Google's Directions Service

In my previous blog post, I talked about how we can use HTML 5 GeoLocation capabilities to build location aware applications with JAXRS and MongoDB at the backend. Today, we will extend the LocalJobs application we built in that blog post with Google's Direction Service. It is recommended that you first read my previous post and then continue with this blog entry. The Directions Web Service allows applications to obtain Driving, Bicycling, and Walking directions through an XML/JSON REST interface. All of the features of the Map API v3 Directions service are supported, including “avoid highways”, “avoid tolls”, and waypoint optimisation.To see the application in action, just go to http://localjobs1-t20.rhcloud.com/. Enter skills sucsh as java , php , mongodb , etc. and press the "Find Jobs" button. The Browser will then ask you to allow the application to use your computer's location. Click on "allow" and you will see results as shown below:

LocalJobs application

Application Source code

The source code of the application is on github at https://github.com/shekhargulati/localjobs-with-google-maps-and-directions-api.

Prerequisite

Before we can start building the application, we'll have to do few setup tasks :

  1. Sign up for an OpenShift Account. It is completely free and Red Hat gives every user three free Gears on which to run your applications. At the time of this writing, the combined resources allocated for each user is 1.5 GB of memory and 3 GB of disk space.

  2. Install the rhc client tool on your machine. The rhc is a ruby gem so you need to have ruby 1.8.7 or above on your machine. To install rhc, just type

sudo gem install rhc

If you already have one, make sure it is the latest one. To update your rhc, execute the command shown below.

sudo gem update rhc

For additional assistance setting up the rhc command-line tool, see the following page: https://openshift.redhat.com/community/developers/rhc-client-tools-install

  1. Setup your OpenShift account using the rhc setup command. This command will help you create a namespace and upload your ssh keys to the OpenShift server.

Let's Build the Application

We will start with spinning up a new instance of JBossEAP and embedding the MongoDB cartridge. To spin up a new instance , type the following command:

$ rhc app create localjobs jbosseap mongodb-2.2

This will create an application container for us, called a gear, and setup all of the required SELinux policies and cgroup configuration. OpenShift will also setup a private git repository for you and clone the repository to your local system. Finally, OpenShift will propagate the DNS to the outside world. The application will be accessible at http://localjobs-domain-name.rhcloud.com/. Replace domain-name with your own unique domain name.

Pull the source code

Next we will delete the default template source code and pull the application source code from my github repository.

$ git rm -rf src pom.xml
$ git commit -am "deleted template files"
$ git remote add upstream git://github.com/shekhargulati/localjobs-with-google-maps-and-directions-api.git
$ git pull -s recursive -X theirs upstream master

Load data into MongoDB

Before we deploy our application to OpenShift, we need to load data into our MongoDB instance that we embedded in a previous step.

On your local machine, run the rhc app show. This command will return the details of your application as shown below.

$ rhc app show -a localjobs
 
localjobs @ http://localjobs-xxx.rhcloud.com/ (uuid: 5195d8fe5973ca386f000083)
-----------------------------------------------------------------------------------
  Created: 12:45 PM
  Gears:   1 (defaults to small)
  Git URL: ssh://5195d8fe5973ca386f000083@localjobs-xxx.rhcloud.com/~/git/localjobs.git/
  SSH:     5195d8fe5973ca386f000083@localjobs-xxx.rhcloud.com
 
  jbosseap-6.0 (JBoss Enterprise Application Platform 6.0)
  --------------------------------------------------------
    Gears: Located with mongodb-2.2
 
  mongodb-2.2 (MongoDB NoSQL Database 2.2)
  ----------------------------------------
    Gears:          Located with jbosseap-6.0
    Connection URL: mongodb://$OPENSHIFT_MONGODB_DB_HOST:$OPENSHIFT_MONGODB_DB_PORT/
    Database Name:  localjobs
    Password:       qySukKdKrZQT
    Username:       admin

Make a note of SSH URL and using the scp command copy the jobs-data.json file to your application gear. You can download the jobs-data.json file from here.

$ scp jobs-data.json <ssh url>:app-root/data 

Next, SSH into your application, using the rhc app ssh command as shown below:

$ rhc app ssh -a localjobs

Once you have ssh'ed into your application gear, change directory to app-root/data. This is the directory where we copied the jobs-data.json file.

$ cd app-root/data

Next, run the mongoimport command to import the data into your MongoDB database.

$ mongoimport -d localjobs -c jobs --file jobs-data.json -u $OPENSHIFT_MONGODB_DB_USERNAME -p $OPENSHIFT_MONGODB_DB_PASSWORD -h $OPENSHIFT_MONGODB_DB_HOST -port $OPENSHIFT_MONGODB_DB_PORT

The command shown above will import 159 job objects to MongoDB.

Finally, we have to create a geospatial index in our jobs collection. MongoDB only supports two dimensional geo spatial indexes. You can only have one geospatial index per collection. By default, 2D geospatial indexes assume longitude and latitude have boundaries of -180 inclusive and 180 non-inclusive (i.e. [-180, 180)). To create a geospatial index, execute the commands shown below.

$ mongo
 
$ use localjobs
 
$ db.jobs.ensureIndex({"location" : "2d"})

Deploy the application

Finally, deploy the application to OpenShift by just doing git push from your local machine.

$ git push

The git push command will push all the source code to your OpenShift gear, and will then execute the maven build. The maven build will create a war file which will be deployed to the JBossEAP application server.

The application will be live at http://localjobs-domain-name.rhcloud.com/. Replace domain-name with your own domain name.

Under the hood

Now that you have the localjobs application running in the cloud, let's look at the source code and understand how things are done. We divide code into two parts -- Backend and Frontend. Let's take a look at both of them one by one.

Backend code

The backend of the application uses the JAXRS and CDI JavaEE 6 technologies. JAXRS allows us to easily publish RESTful web services and CDI allows us to use dependency injection in a JavaEE environment. The application uses MongoDB to store the data.

The only interesting part of the backend code is JobsRestService. The service shown below has just one REST Endpoint available at http://localjobs-domain-name/api/jobs/{skills}?longitude={longitude}&latitude={latitude}. This REST endpoint says - find me all the jobs near the given latitude and longitude with the following skills.

 @GET
    @Path("/{skills}")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Job> allJobsNearToLocationWithSkill(
            @PathParam("skills") String skills,
            @QueryParam("longitude") double longitude,
            @QueryParam("latitude") double latitude) {
 
        String[] skillsArr = skills.split(",");
        BasicDBObject cmd = new BasicDBObject();
        cmd.put("geoNear", "jobs");
        double lnglat[] = { longitude, latitude };
        cmd.put("near", lnglat);
        cmd.put("num", 5);
        BasicDBObject skillsQuery = new BasicDBObject();
        skillsQuery.put("skills",
                new BasicDBObject("$in", Arrays.asList(skillsArr)));
        cmd.put("query", skillsQuery);
        cmd.put("distanceMultiplier", 111);
 
        System.out.println("Query -> " + cmd.toString());
        CommandResult commandResult = db.command(cmd);
 
        BasicDBList results = (BasicDBList)commandResult.get("results");
        List<Job> jobs = new ArrayList<Job>();
        for (Object obj : results) {
            Job job = new Job((BasicDBObject)obj);
            jobs.add(job);
        }
        System.out.println("Result : " + jobs);
 
        return jobs;
    }

The code shown above creates a MongoDB geonear query where jobs should have any of skills mentioned in the @PathParam and number of results should be limited to 5. The distanceMultiplier does the conversion of radians to miles or kilometers. The 111 value corresponds to kilometers. Then, we execute the query against the database and get CommandResult. We then iterate over all the results and create a value object called "Job" and return the List of Job objects back. Finally, the List will be converted to JSON objects and will be consumed by the clients.

You can use curl or a browser to test the REST endpoint. Below you can see the curl command to fetch all the php jobs near a given latitude and longitude:

curl http://localjobs1-t20.rhcloud.com/api/jobs/php/?longitude=76.776697&latitude=30.378179
 
[{"id":"513197f430040316a9393dcb","companyName":"ABC2 Demo Pvt.Ltd","jobTitle":"PHP Programmers","distance":841.4285333943001,"skills":["php","mongodb"],"formattedAddress":"563/1A Sarasavi Mawatha Nawala-Rajagiriya, Colombo","longitude":79.89967890000003,"latitude":6.9072435},{"id":"513197f430040316a9393d82","companyName":"Intuitent Online","jobTitle":"PHP developer with 2+ years of experience for exciting role in startup","distance":3159.6748649615424,"skills":["php"],"formattedAddress":"9106, Garden Villas, DLF Phase IV,, Gurgaon, Haryana, India","longitude":77.090554,"latitude":28.463809}]

Frontend Code

Now lets move to the interesting part, which is the Frontend of the application. The frontend of the application is built using Twitter Bootstrap and jQuery. This is a single page web application with all the HTML residing in index.html.

The index.html is a HTML5 page as you can see from its doctype. It has one form where users can enter the skills and a div with results where different jobs will be rendered. Apart from this, it imports all the required CSS and JavaScript files. When the document is ready, we call the tagsInput method on the skills input box, which will turn the boring tag list into a magical input that turns each tag into a style-able object with its own delete link. This is done using jQuery tags input plugin. Also, we check whether the browser supports Geo Location API or not. If the browser does not support Geo Location API, you will not be able to use the application.

<!DOCTYPE html>
<html>
<head>
<title>LocalJobs : Find jobs near to you</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<style>
body {
    padding-top: 60px;
    padding-bottom: 100px;
}
</style>
<link href="css/bootstrap.css" rel="stylesheet">
<link href="css/bootstrap-responsive.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="css/jquery.tagsinput.css" />
<link href="css/jquery.loadmask.css" rel="stylesheet" type="text/css" />
 
</head>
<body>
 
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="navbar-inner">
            <div class="container">
                <button type="button" class="btn btn-navbar" data-toggle="collapse"
                    data-target=".nav-collapse">
                    <span class="icon-bar"></span> <span class="icon-bar"></span> <span
                        class="icon-bar"></span>
                </button>
                <a class="brand" href="#">LocalJobs</a>
                <div class="nav-collapse collapse">
                    <ul class="nav">
                        <li class="active"><a href="#">Home</a></li>
                    </ul>
                </div>
                <!--/.nav-collapse -->
            </div>
        </div>
    </div>
 
    <div id="main" class="container">
 
 
        <form id="jobSearchForm" class="form-horizontal">
            <div class="control-group">
                <div class="controls">
                    <input type="text" id="skills" name="skills" class="input-xlarge"
                        placeholder="Enter skills for which you want to search jobs"
                        required>
                </div>
            </div>
 
            <div class="control-group">
                <div class="controls">
                    <button id="findJobsButton" type="submit" class="btn btn-success">Find
                        Jobs</button>
                </div>
            </div>
 
        </form>
 
        <div id="results">
        </div>
 
 
 
    </div>
 
 
    <script src="js/jquery.js"></script>
    <script src="js/jquery.tagsinput.js"></script>
    <script type="text/javascript" src="js/jquery.loadmask.min.js"></script>
    <script
        src="https://maps.googleapis.com/maps/api/js?sensor=true"></script>
    <script src="js/bootstrap.js"></script>
 
    <script type="text/javascript">
    $( document ).ready( function() {
        $('#skills').tagsInput({
            defaultText : "add skills"
        });
    });
    if(!navigator.geolocation){
        alert('Your browser does not support geolocation. Please download latest browser version.');
        $("#findJobsButton").attr("disabled", "disabled");
    }
    </script>
 
    <script src="js/main.js"></script>
</body>
</html>

The application specific JavaScript code is in main.js.

When a user submits the form, a function is called which empties the results div, gets the skills from the input box,and calls the getCurrentPosition function as shown below:

$("#jobSearchForm").submit(function(event){
    console.log("Submitted Job search form");
    event.preventDefault();
    $("#results").empty();
    $("#jobSearchForm").mask("Finding Jobs ...");
 
    var skills = $('input[name=skills]').val().split(',');
 
    console.log("skills : "+skills);
 
    getCurrentPosition(callback , skills);
 
});

The getCurrentPosition function uses HTML 5 GeoLocation API to fetch the latitude and longitude of the user and executes the callback function passing it the latitude, longitude, and skills as shown below:

function getCurrentPosition(callback , skills){
 
    navigator.geolocation.getCurrentPosition(function(position){
 
                    var longitude = position.coords.longitude;
                    var latitude = position.coords.latitude;
                    callback(latitude , longitude , skills);
                }, function(e){
                    $("#jobSearchForm").unmask();
                    switch (e.code) {
                        case e.PERMISSION_DENIED:
                            alert('You have denied access to your position. You will                                ' + 'not get the most out of the application now.'); 
                            break;
                        case e.POSITION_UNAVAILABLE:
                            alert('There was a problem getting your position.'); 
                            break;
                        case e.TIMEOUT:
                                    alert('The application has timed out attempting                                     to get your location.'); 
                            break;
                        default:
                            alert('There was a horrible Geolocation error that has ' +'not been defined.');
                    }
                },
                    { timeout: 45000 }
 
                );
}

The callback function makes a get call to fetch all the jobs near the given latitude and longitude with the required skills. The response of the get call is given to the renderAllJobs function, which renders all the jobs one by one.

function callback(latitude , longitude , skills){
 
    console.log('longitude .. '+longitude);
    console.log('latitude .. '+latitude);
    var userLocation = new google.maps.LatLng(latitude , longitude);
    $.get("api/jobs/"+skills+"/?longitude="+longitude+"&latitude="+latitude  , function (jobs){ 
         $("#jobSearchForm").unmask();
         renderAllJobs(jobs , userLocation);
    });
}
 
function renderAllJobs(jobs , userLocation){
    var directionsService = new google.maps.DirectionsService();
    $.each(jobs , function(index , job){
        renderJob(job , userLocation , directionsService);
    });
}

Finally, for each job object, a map is created and a call is made to get the route from a user's current location to the job location. The call is made using google.maps.DirectionsService which we instantiated in renderAllJobs function. The DirectionService route method takes two arguments -- request object and callback function. The request object is used to specify the origin and destination positions or locations. The callback function is invoked when the call to route method finishes. We check if the status is OK and then we ask google.maps.DirectionsRenderer to render it on the map as well as on the scrollable div.

function renderJob(job , userLocation , directionsService){
    var jobRow = "<div class='row'>";
    jobRow += "<h2>"+job.jobTitle+" at "+job.companyName+"</h2>";
    jobRow += "<div id='routeMap-"+job.id+"' class='span6' style='height: 500px'></div>";
    jobRow += "<div id='directionsPanel-"+job.id+"' class='span5 offset1' style='height: 500px;overflow:scroll'></div>";
    jobRow += "</div>";
    $('#results').append(jobRow);
 
    var mapOptions = {
                      zoom: 3,
                      center: userLocation,
                      mapTypeControlOptions: {
                         style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
                       },
                      mapTypeId: google.maps.MapTypeId.ROADMAP,
                      zoomControlOptions: {
                          style: google.maps.ZoomControlStyle.SMALL
                      }
            };
 
    var map = new google.maps.Map(document.getElementById('routeMap-'+job.id),
                          mapOptions);
 
    var directionRenderer = new google.maps.DirectionsRenderer({suppressMarkers: true});
    directionRenderer.setMap(map);
 
    // Start/Finish icons
 var icons = {
  start: new google.maps.MarkerImage(
   // URL
   'http://icons.iconarchive.com/icons/icons-land/vista-people/48/Office-Customer-Male-Light-icon.png',
   // (width,height)
   new google.maps.Size( 44, 32 ),
   // The origin point (x,y)
   new google.maps.Point( 0, 0 ),
   // The anchor point (x,y)
   new google.maps.Point( 22, 32 )
  ),
  end: new google.maps.MarkerImage(
   // URL
   'http://icons.iconarchive.com/icons/mad-science/olive/32/Martinis-Briefcase-icon.png',
   // (width,height)
   new google.maps.Size( 44, 32 ),
   // The origin point (x,y)
   new google.maps.Point( 0, 0 ),
   // The anchor point (x,y)
   new google.maps.Point( 22, 32 )
  )
 };
 
    var request = {
                   origin : userLocation,
                   destination : new google.maps.LatLng(job.latitude, job.longitude),
                   travelMode : google.maps.DirectionsTravelMode.DRIVING,
                   unitSystem: google.maps.UnitSystem.METRIC
    };
 
    directionsService.route(request , function(result , status){
        if(status == google.maps.DirectionsStatus.OK){
            directionRenderer.setDirections(result);
            var leg = result.routes[0].legs[0];
             makeMarker( map , leg.start_location, icons.start, "title" );
             makeMarker( map , leg.end_location, icons.end, 'title' );
 
            directionRenderer.setPanel(document.getElementById("directionsPanel-"+job.id));
        }
    });
 
}
 
function makeMarker( map , position, icon, title ) {
 new google.maps.Marker({
  position: position,
  map: map,
  icon: icon,
  title: title
 });
}

That is all what is required for rendering directions in your application.

Conclusion

In this blog, we looked at how we can use the Google Maps Direction Service to augment our location aware applications.

What's Next?