How To Build Location-Aware Web Applications using HTML5 and MongoDB

We all use location aware applications in our day-to-day life. Applications like Foursquare, Facebook places, etc help us share our location (or places we visit) with our friends and family. Applications such as Google Local help us find out which businesses are near our current location. So, if we need a good coffee shop recommendation, we can ask Google Local to find us all the coffee shops near our location. This not only helps the customer, but also helps businesses reach the right audience and sell their products more effectively. It is a win-win situation for both consumers and businesses.

To build such an application , you need the geolocation of the user. According to Wikipedia, "Geolocation is the identification of the real-world geographic location of an object". Until now, there was no standard way to find the location of a user in a web application. We could use an open source library like Google Gears to get the geo location of a user but this library is not under development anymore and should only be used with older browsers, which do not support W3C GeoLocation API. The W3C GeoLocation API is a specification that provides standard scripted access to geographical information associated with the hosting device. Geo Location support is not officially part of HTML 5 specification but people use it interchangeably and most commonly you will hear about GeoLocation APIs with respect to HTML5. This API provides an abstraction layer on top of how geolocation information of a user is gathered. All modern browsers support the GeoLocation API. The below table is taken from http://caniuse.com/#feat=geolocation.

GeoLocation API Browser Support

Application Use case - Job Search Application

In this blog, we will build a location aware job search application. A user specifies the skills (java , scala , mongodb, etc) and the application finds the nearest jobs with those skills. The users location is found using the W3C GeoLocation API. Then, the application plots the user location and jobs on a Google Map. The application is live at http://localjobshtml5-cix.rhcloud.com/. The user icon corresponds to the users geolocation and the briefcase corresponds to a job.

LocalJobs application

If you click on any job i.e. briefcase , the map will zoom in as shown below. And when you close the information window, it will again zoom out. Also, you will see the distance from your location to the job location , job title , and other related information on the marker. The distance between the users location and the job location is found using MongoDB geospatial support, which we will talk about it later in this post.

LocalJobs Jobs Zoom in

Application Technology Stack

The application will be built using the following technology stack :

  1. Java EE 6 : We will be using a few Java EE 6 specs -- JAX-RS and CDI. JAX-RS stands for Java API for Restful Web Services. It provides a Java API for creating web services according to the REST architectural pattern. CDI stands for Context and Dependency Injection. CDI allows Java EE components to be bound to lifecycle contexts, injected, and then interact in a loosely coupled way by firing and observing events.

  2. MongoDB : MongoDB is a NoSQL document oriented datastore. We will persist the jobs data in MongoDB and will be using its geo spatial capabilities in the application.

  3. HTML 5 : The client side of the application is built using HTML 5. We will also use the W3C GeoLocation API to get the users current location.

  4. Google Maps : The application will be using Google Maps to render the user and jobs information.

  5. OpenShift : The application will be deployed to the OpenShift public PaaS.

Application Source code

The source code for this application is on github at https://github.com/shekhargulati/localjobshtml5

Prerequisite

Before we can start building the application, we'll have to do a 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 tools 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 tools, execute the command shown below:

sudo gem update rhc

For additional assistance setting up the rhc command-line tool, see the following page: Install RHC client tools.

  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

Now that we are done with all of the setup tasks, let's start building our application. We will start with creating an OpenShift application. One thing that you should understand when you work with any PaaS is that PaaS is all about creating applications. So, rather than talking in terms of virtual machines or servers you talk in terms of applications.

Create JBossEAP MongoDB OpenShift Application

To create an application named 'localjobs' which should use JBossEAP and MongoDB , we will execute 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.

The command shown above will create a stock-standard template Maven project. The only interesting thing is that in the pom.xml file there is a profile named openshift as shown below. So, when you push your source code to OpenShift, this Maven profile is executed . This profile does not do anything special -- it just creates a war file with name ROOT so that your application is available over root context.

<profiles>
        <profile>
            <id>openshift</id>
            <build>
                <finalName>localjobs</finalName>
                <plugins>
                    <plugin>
                        <artifactId>maven-war-plugin</artifactId>
                        <version>2.1.1</version>
                        <configuration>
                            <outputDirectory>deployments</outputDirectory>
                            <warName>ROOT</warName>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

Next, we will remove the index.html and snoop.jsp files from our git repoistory since we don't need them. If you are not familiar with git you can read this great tutorial by Lars Vogel.

git rm -f src/main/webapp/index.html src/main/webapp/snoop.jsp 
 
git commit -am "deleted template files"

Adding MongoDB Java Driver Dependency

The pom.xml created by OpenShift already has all Java EE 6 related dependencies. In order to use MongoDB, we need to add the MongoDB Java driver dependency. I am using the latest version of the MongoDB Java driver. Add the following dependency to pom.xml file. You can view the full pom.xml on github.

<dependency>
    <groupId>org.mongodb</groupId>
    <artifactId>mongo-java-driver</artifactId>
    <version>2.10.1</version>
</dependency>

Enable CDI

CDI stands for Contexts and Dependency Injection. We are using CDI in our application so that we can use dependency injection instead of manually creating the objects ourselves. The CDI container will manage the bean lifecycle and we as developers just write the business logic. To let the JBossEAP application server know that we are using CDI, we need to create a file - beans.xml - in our WEB-INF directory.The file can be completely blank, but it's presence tells the container that the CDI framework needs to be loaded. The beans.xml file is shown below:

<?xml version="1.0"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://jboss.org/schema/cdi/beans_1_0.xsd"/>

Write a MongoDB Database Connection Class

Next, we will create an application scoped bean to manage MongoDB database connections. The connection class works on both the local system as well as on OpenShift. You can view the full class on github.

@ApplicationScoped
public class DBConnection {
 
    private DB mongoDB;
 
    @PostConstruct
    public void afterCreate() {
        System.out.println("just see if we can say anything");
 
        String host = System.getenv("OPENSHIFT_MONGODB_DB_HOST");
 
        if (host == null || "".equals(host)) {
            // Create Local MongoDB Connection
        } else {
            String mongoport = System.getenv("OPENSHIFT_MONGODB_DB_PORT");
            String user = System.getenv("OPENSHIFT_MONGODB_DB_USERNAME");
            String password = System.getenv("OPENSHIFT_MONGODB_DB_PASSWORD");
            String db = System.getenv("OPENSHIFT_APP_NAME");
            int port = Integer.decode(mongoport);
 
            Mongo mongo = null;
            try {
                mongo = new Mongo(host, port);
            } catch (UnknownHostException e) {
                System.out.println("Couldn't connect to Mongo: "
                        + e.getMessage() + " :: " + e.getClass());
            }
 
            mongoDB = mongo.getDB(db);
 
            if (mongoDB.authenticate(user, password.toCharArray()) == false) {
                System.out.println("Failed to authenticate DB ");
            }
        }
 
    }
 
    @Produces
    public DB getDB() {
        return mongoDB;
    }
 
 
}

An @ApplicationScoped bean will live for as long as the application is running and is destroyed when the application is shut down. This is exactly the behavior we want for the object holding on to the pooled connections from the MongoDB driver.

Write RESTful Backend

Now we will move to writing a RESTful backend for our application using JAX-RS. We will activate JAX-RS by creating a class which extends javax.ws.rs.ApplicationPath. You need to specify the base url under which your web service will be available. This is done by annotating the class with the ApplicationPath annotation. In the code shown below, I have used "/api" as the base URL:

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
 
@ApplicationPath("/api")
public class JaxRsActivator extends Application {
   /* class body intentionally left blank */
}

Once we have activated JAX-RS, we can write our REST service. 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 will find all the jobs near the given latitude and longitude with the following skills.

@Path("/jobs")
public class JobsRestService {
 
    @Inject
    private DB db;
 
    @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", 10);
        BasicDBObject skillsQuery = new BasicDBObject();
        skillsQuery.put("skills",
                new BasicDBObject("$in", Arrays.asList(skillsArr)));
        cmd.put("query", skillsQuery);
        cmd.put("distanceMultiplier", 111);
 
        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);
        }
        return jobs;
    }
}

The code shown above creates a MongoDB geo near query on the jobs collection. It limits the number of documents in the results to 10. MongoDB returns result in the same value as your data. Because our latitude and longitude are in degrees, the returned data is also in degrees. However, MongoDB gives you an option to specify your distance multiplier which helps in converting degrees to kilometers or miles. In the code above, I have used the distance multiplier as 111, which converts degrees to kilometers. Finally, we convert the data to a domain object called Job and return it back. The @Produces annotation will make sure that data will be converted to JSON.

Load Data into MongoDB

Execute the following commands to load data into MongoDB running on your OpenShift gear.

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-newideas.rhcloud.com/ (uuid: 5195d8fe5973ca386f000083)
-----------------------------------------------------------------------------------
  Created: 12:45 PM
  Gears:   1 (defaults to small)
  Git URL: ssh://5195d8fe5973ca386f000083@localjobs-newideas.rhcloud.com/~/git/localjobs.git/
  SSH:     5195d8fe5973ca386f000083@localjobs-newideas.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 the 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.

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

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

$ rhc app ssh -a localjobs

Once you have ssh'ed into your application gear, change directories 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 geospatial index in our jobs collection. MongoDB only supports two-dimensional geospatial 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"})

Test RESTful Service

Next, we will commit the source code and push the changes to OpenShift. This will build the project, create a new war file, and deploy that to JBossEAP running on OpenShift.

$ git add .
$ git commit -am "RESful backend done"
 
$ git push

After the code is built and the war file is deployed we can test the REST service using curl.

curl -i -H "Accept: application/json" http://localjobs-newideas.rhcloud.com/api/jobs/java,scala?longitude=-121.894955&latitude=37.339386
 
 
HTTP/1.1 200 OK
Date: Fri, 17 May 2013 08:39:11 GMT
Server: Apache-Coyote/1.1
Content-Type: application/json
Vary: Accept-Encoding
Transfer-Encoding: chunked
 
[{"companyName":"CyberCoders","jobTitle":"Embedded Java Applications Engineer","distance":4153.025944882882,"skills":["java"],"formattedAddress":"1400 North Shoreline Boulevard, Mountain View, CA, United States","longitude":-122.078488,"latitude":37.414198},{"companyName":"CyberCoders","jobTitle":"Embedded Java Applications Engineer","distance":4153.025944882882,"skills":["java"],"formattedAddress":"1400 North Shoreline Boulevard, Mountain View, CA, United States","longitude":-122.078488,"latitude":37.414198}
.....
]

Beautify the Application

Now that we have confirmed that our application REST service is working fine, let's build the UI of the application. The application UI that we are building is very simple, we will have a form where the user can enter skills and we will have a div where a Google Map will be rendered with jobs and the users location. Create an index.html under src/main/webapp folder as shown below:

<!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;
}
 
#map-canvas {
    height: 500px;
    width: 100%;
}
.job_info {border: 1px solid #000;padding: 15px;width: 300px}
.job_info h3 {margin-bottom: 10px}
</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>
 
    <div id="map-canvas"></div>
 
    <script type="text/x-mustache-template" id="job-template">
 
 
    <div class="jobBox">
       <h3>{{jobtitle}}</h3>
       <p> {{company}} </p>
      <address> {{address}} </address>
      <p> {{skills}}</p>
      <p> {{distance}} </p>
    </div>
    </script>
 
    <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 src="js/underscore.js"></script>
    <script src="js/backbone.js"></script>
    <script src="js/mustache.js"></script>
 
    <script type="text/javascript">
    $( document ).ready( function() {
        $('#skills').tagsInput({
            defaultText : "add skills"
        });
    });
    </script>
 
    <script src="js/app.js"></script>
</body>
</html>

The index.html shown above is a HTML 5 file and it uses the HTML 5 doctype . The application uses Twitter Bootstrap, which is a free collection of tools for creating websites and web applications. It contains HTML and CSS-based design templates for typography, forms, buttons, charts, navigation and other interface components, as well as optional JavaScript extensions. You can get all the related css,js files from project github repository.

Check GeoLocation Support

Because our application depends on the user's location, which we will be getting using GeoLocation API, it makes sense to check if the user browser supports GeoLocation API. To check that the user's browser supports GeoLocation API, add the following to document ready function as shown below. If the user's browser supports GeoLocation, then the navigator object will have geolocation object. You can also use an open source library like Modernizr to detect HTML5 features. If the browser does not support geolocation, then we disable the form submit button.

<script type="text/javascript">
    if(!navigator.geolocation){
        alert('Your browser does not support geolocation. Please download latest browser version.');
        $("#findJobsButton").attr("disabled", "disabled");
    }
    </script>

Find Jobs on Form Submission

Now that we know that the user's browser supports GeoLocation API, the next logical thing to do is to find jobs for skills specified by that user. This project uses Backbone.js to give structure to our client side code. If you are not aware of backbone.js then please refer to my previous blog, Building Single Page Web Applications with Backbone.js, JaxRS, MongoDB, and OpenShift where I talk about how to create an application using backbone.js. Please copy the app.js file to js folder under src/main/webapp. Below is the trimmed down version of app.js file. I have removed some parts for brevity.

// app.js
(function($){
 
        var LocalJobs = {};
        window.LocalJobs = LocalJobs;
 
        var template = function(name) {
            return Mustache.compile($('#'+name+'-template').html());
        };
 
        LocalJobs.HomeView = Backbone.View.extend({
            tagName : "form",
            el : $("#main"),
 
            events : {
                "submit" : "findJobs"
            },
 
            render : function(){
                console.log("rendering home page..");
                $("#map-canvas").empty();
                return this;
            },
 
            findJobs : function(event){
                event.preventDefault();
                $("#map-canvas").empty();
                $("#jobSearchForm").mask("Finding Jobs ...");
                var skills = this.$('input[name=skills]').val().split(',');
 
                console.log("skills : "+skills);
 
                var self = this;
 
                  var mapOptions = {
                    zoom: 3,
                    center: new google.maps.LatLng(-34.397, 150.644),
                    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('map-canvas'),
                      mapOptions);
 
 
                navigator.geolocation.getCurrentPosition(function(position){
                    var longitude = position.coords.longitude;
                    var latitude = position.coords.latitude;
                    console.log('longitude .. '+longitude);
                    console.log('latitude .. '+latitude);
 
                    $("#jobSearchForm").unmask();
                    self.plotUserLocation(new google.maps.LatLng(latitude, longitude),map);
 
                    $.get("api/jobs/"+skills+"/?longitude="+longitude+"&latitude="+latitude  , function (results){ 
                            $("#jobSearchForm").unmask();
                            self.renderResults(results,self,map);
                     });
                }, function(e){
                    $("#jobSearchForm").unmask();
                        // handle error
 
                            },
                    { timeout: 45000 }
 
                );
 
            },
 
            plotUserLocation : function(latLng , map){
 
            },
 
            renderResults : function(results,self,map){
                var infoWindow = new google.maps.InfoWindow();
                _.each(results,function(result){
                    self.renderJob(result,map , infoWindow);
                });
 
            },
 
        renderJob : function(result , map , infoWindow){
        }
 
 
 
 
        });
 
 
        LocalJobs.Router = Backbone.Router.extend({
            el : $("#main"),
 
            routes : {
                "" : "showHomePage"
            },
            showHomePage : function(){
                console.log('in home page...');
                var homeView = new LocalJobs.HomeView();
                this.el.append(homeView.render().el);
            }
 
        });
 
        var app = new LocalJobs.Router();
        Backbone.history.start();
 
 
})(jQuery);

Let's understand what this code does.

  1. The code shown above creates an instance of a backbone router providing it the root DOM as the main div. Then when we hit the base url, the router calls the showHomePage function which renders the HomeView. The HomeView in its rendered function empties the div with id map-canvas.

  2. In HomeView we have an event listener for form submissions. So when users press the submit button after entering skills, the findJobs function will be called.

  3. The findJobs function is where everything happens.

    3.1 First we get the value of input type with name skills. And then split them by comma, so that we have an array of skills.

    3.2 Then we create a Google Map object providing it some default values.

    3.3 Next we call the getCurrentPosition method on navigator.geolocation object. This method has only one required parameter -- success_callback and two optional parameters -- error_callback and PositionOptions object are optional.

    3.4 If the getCurrentPosition call succeeds, then success_callback is called. This callback has one argument -- position. The position object holds the latitude and longitude of a user. Then the user location is plotted on the map.

    3.5 After the users location is plotted, a get call is made using jQuery.

    3.6 Finally all the results are iterated and shown on the map.

Push the Code

Now you can push the code to OpenShift and see your application running in cloud.

git add .
git commit -am "localjobs app with UI"
git push

The application will be running at https://localjobs-domain-name.rhcloud.com/. Please replace domain-name with your own namespace.

Conclusion

In this blog post, we looked at how we can use the HTML 5 GeoLocation API and MongoDB Geo spatial indexing capabilities to build location aware applications.

What's Next?

Excellent post, geospatial indexes in action!! thanks