NoSQL Zone is brought to you in partnership with:

Java Geek and managing director@comSysto GmbH in Munich ... Spring over JavaEE, Apache Wicket over JSF, Gradle over Maven, Lean over Waterfall, exploring JavaFX, Highcharts, Android, AgileUX, Lean Startup. Daniel is a DZone MVB and is not an employee of DZone and has posted 41 posts at DZone. You can read more from them at their website. View Full User Profile

Using MongoDB Geospatial with Spring Data and a Basic JQuery Mobile UI

11.23.2012
| 8596 views |
  • submit to reddit

 

tracks-android-track-details Using MongoDB Geospatial with Spring Data and a basic JQuery Mobile UI

This article shows how to use the MongoDB spatial feature in combination with Spring and a Web REST service.

At first let us introduce the purpose of the example web application. Our application stores track informations about tracks an user has recorded with a GPS tracking device. For simplification we assume that the tracking data exists in a JSON format. This document is stored in the MongoDB with the additional information about the user who uploaded the track and an additional attribute about the position where the track starts.

With that track information stored in the MongoDB we start a search, that includes all tracks with startpositions within a certain radius. These Tracks are shown on a map.

The MongoDB part

If you have not installed MongoDB yet, download the latest version and install and start it. Now we have to start the interactive mongo shell. In the console you type following commands shown in Listing 1 to use the track collection and create a mongo spatial index.

use tracks;
db.track.ensureIndex( { start : "2d" } );

Listing 1: The command to creaet a MongoDB spatial index.

Now you can use the MongoDB spaitial feature. For example you can execute the command in Listing 2 to find the tracks with a nearby search to a given point.

db.track.find( { start : { $near : [11.531439781188965,48.156700134277344] } } ).limit(2)

Listing 2: Find the two nearst tracks to the given point with the latitude and longitude.

Or you can execute the command in Listing 3 to find the tracks with a nearby search to a given point with a maximum distance. In the example we search in a distance of 1 kilometer. Therefore we have to divide the value 1 by 111 to get a maximum distance of 1 kilometer. For 1 mile we have to divide the value by 69.

db.track.find( { start : { $near : [11.531439781188965,48.156700134277344] , $maxDistance : 0.009009009009009 } } ).limit(2);

Listing 3: Find the two nearest tracks to a given point with a maximum distance of 1 kilometer.

JSON Document and matching Java types

Lets assume the track information is a JSON document and has the structure shown in listing 4.

{
    "name": "Nymphenburger Schloss",
    "start": { "lon":11.53144, "lat":48.1567, "ele":520 },
    "user": "joe@joe.com",
    "data":
    [
       {"lon":11.53144, "lat":48.1567, "ele":520},
       {"lon":11.53125, "lat":48.15672, "ele":520},
   ... // additional track information
    ]
}

Listing 4: JSON document structure

The corresponding Java object is shown in listing 5.

public class Track  implements Serializable{

private String id;
private String name;
private Position start;
private String user;
private List<Position> data;

// ... getter, setter, etc. ...

}

Listing 5: Show the JSON document corresponding Java object

The serializable interface is not necessarily required  in this example but maybe you want to use another frontend technology like wicket. Then at least you have to add the interface to your Java object.

The next listing 6 shows the Java object position that is used in the Java object track.

public class Position implements Serializable {

private Double lon;
private Double lat;
private Double ele;
// ... getter, setter, etc. ...
}

Listing 6: The position that is used in the track.

In this short example the user is not a Java object it’s only a attribute in the track that has a unique identifier.

Now we are ready to set up a Spring configuration to use MongoDB.

The Spring REST Web Service

In the first part we introduced the objects we want to store in the MongoDB. Now we need a service to do that for us. Lets have a look at the configuration and implementation of a REST Web service for this example.

The Listing 7 shows the configuration that is necessary in the web.xml to get Spring and and our Rest Service going.

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>

<display-name>Mobile Mongo Application</display-name>

<context-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>classpath:com/comsysto/labs/mobile/tracks/applicationContext-security.xml</param-value>
</context-param>

	<listener>
		<listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
	</listener>

	<servlet>
		<servlet-name>service</servlet-name>
		<servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:com/comsysto/labs/mobile/tracks/applicationContext.xml</param-value>
</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>service</servlet-name>
		<url-pattern>/services/*</url-pattern>
	</servlet-mapping>

</web-app>

Listing 7: Shows the configuration of Spring and a REST Web service in the web.xml

With that configuration every request that calls an url under the path of /services delegetas to spring. For the servlet mapping with the name services a corresponding file with the name services-servlet.xml must be created. In that file nothing has to be configured. The interesting part is the applicationContext.xml file that is placed in the the folder src/main/resources/com/comsysto/labs/mobile/tracks/.

Listing 8 show the confiuguration of the applicationContext.xml. This is the base Spring configuration file in this example.

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>

 <!-- To enable Spring MVC -->
    <mvc:annotation-driven/>

    <!-- Scan all the classes in this package for autodetection. Here are our
   <!-- Service classes with the annotation @Controller are placed. -->
    <context:component-scan base-package="com.comsysto.labs.mobile.tracks.rest"/>

    <!-- The MongoDB Database connection -->
    <mongo:mongo id="mongo"/> <!-- defaults: host="127.0.0.1" port="27017"-->

    <!-- The primary implementation of the interface MongoOperations that    -->
    <!-- specifies a basic set of MongoDB operations. Here configured to use -->
    <!-- the collection tracks. -->
    <bean id="tracksMongoOperations"
          class="org.springframework.data.mongodb.core.MongoTemplate">
        <constructor-arg>
            <ref bean="mongo"/>
        </constructor-arg>
        <constructor-arg value="tracks"/>
    </bean>

</beans>

Listing 8: Shows the configuration of the applicationContext.xml

Now lets come to the part where everything glues together. Therefore we have a look in our TrackService class shown in Listing 9 in the package org.comsysto.labs.web, where the services are placed which are autodetected by the component scan.

@Controller
@RequestMapping("/track")
public class TrackService {

    @Autowired
    @Qualifier("tracksMongoOperations")
    public MongoOperations mongoOperations;

    public static final Double KILOMETER = 111.0d;

    /**
     * The Attribute that is used for the search for the start position
     */
    public static final String START = "start";
    /**
     * The Attribute that is used for the search for the user
     */
    private static final String USER = "user";

    @RequestMapping(value = "/get", method = RequestMethod.GET,
            produces = "application/json")
    public
    @ResponseBody
    List<Track> getAll() throws Exception {
        return mongoOperations.findAll(Track.class);
    }

    @RequestMapping(value = "/get/{lon1}/{lat1}/{lon2}/{lat2}/",
            method = RequestMethod.GET, produces = "application/json")
    public
    @ResponseBody
    List<Track> getByBounds(@PathVariable("lon1") Double lon1,
                            @PathVariable("lat1") Double lat1,
                            @PathVariable("lon2") Double lon2,
                            @PathVariable("lat2") Double lat2) throws Exception {
        /**
         > box = [[40.73083, -73.99756], [40.741404,  -73.988135]]
         > db.places.find({"loc" : {"$within" : {"$box" : box}}})
         **/
        Criteria criteria = new Criteria(START).within(new Box(new Point(lon1, lat1),
                new Point(lon2, lat2)));
        List<Track> tracks = mongoOperations.find(new Query(criteria),
                Track.class);
        return tracks;
    }

    @RequestMapping(value = "/get/{lon}/{lat}/{maxdistance}",
            method = RequestMethod.GET, produces = "application/json")
    public
    @ResponseBody
    List<Track> getByLocation(@PathVariable("lon") Double lon,
                              @PathVariable("lat") Double lat,
                              @PathVariable("maxdistance") Double maxdistance)
            throws Exception {
        Criteria criteria = new Criteria(START).near(new Point(lon, lat)).
                maxDistance(getInKilometer(maxdistance));
        List<Track> tracks = mongoOperations.find(new Query(criteria),
                Track.class);
        return tracks;
    }

    /**
     * The current implementation of near assumes an idealized model of a flat
     * earth, meaning that an arcdegree
     * of latitude (y) and longitude (x) represent the same distance everywhere.
     * This is only true at the equator where they are both about equal to 69 miles
     * or 111km. Therefore you must divide the
     * distance you want by 111 for kilometer and 69 for miles.
     *
     * @param maxdistance The distance around a point.
     * @return The calcuated distance in kilometer.
     */
    private Double getInKilometer(Double maxdistance) {
        return maxdistance / KILOMETER;
    }

    @RequestMapping(value = "/add", method = RequestMethod.POST,
            consumes = "application/json")
    @ResponseStatus(HttpStatus.OK)
    public void add(@RequestBody Track track) throws Exception {
        mongoOperations.insert(track);
    }

    @RequestMapping(value = "/foruser", method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    public
    @ResponseBody
    List<Track> tracksForUser(@RequestParam("user") String user)
            throws Exception {
        Criteria criteria = Criteria.where(USER).is(user);
        List<Track> tracks = mongoOperations.find(new Query(criteria),
                Track.class);
        return tracks;
    }

    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void upload(@RequestParam("file") MultipartFile multipartFile)
            throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        Track track = mapper.readValue(multipartFile.getBytes(), Track.class);
        mongoOperations.insert(track);
    }
}

Listing 9: The implementation of the TrackService.

The context component scan scans for all objects with the @Controller annotation and that is the target of our DispatcherServlet mapped in the web.xml under the path /services. The @RequestMapping(“/track”) maps the whole service under an additional sub-path /track. The annotation at the method getAll @RequestMapping(value = “/get”) maps the Method again. This means to call the getAll Method we have to use the path /services/track/get/.

With the attriubte method = RequestMethod.GET we define a Method that is only executed via a get request. The attribute produces = “application/json” defines that the result of the method is JSON. The result is automatically converted from a Java object to JSON by Spring. Therefore shown in listing 10  the Jar  file Jackson must be present on the classpath.

<dependency>
     <groupId>org.codehaus.jackson</groupId>
     <artifactId>jackson-jaxrs</artifactId>
     <version>1.9.5</version>
</dependency>

Listing 10: Shows the Maven dependency for Jackson

The other methods are really straight forward and hopefully need not furhter documentation.

The UI

So now that we have a nice REST-Service we need some kind of UI. As we wanted to try some tools and frameworks we haven’t used yet, we decided to build a browser based app for mobile devices. So for this prototype we used:

You may have guessed the app we are building is basically a clone of one of the many sites like GPSies or bikemap.net where you can share your gps tracks.

As our time was limited we started with four basic features:

  1. Upload tracks
  2. View tracks near your current location on a map
  3. View details of a track
  4. View the list of tracks you have uploaded

Each of the features (+ one main menu) requires an own page in the app, so we start with a JQuery Mobile multipage:

  <body>
      <div data-role="page" id="home" style="width:100%; height:100%;">
          <div data-role="header" data-position="fixed"><h2>cS Mobile Tracks</h2></div>
          <div data-role="content" style="padding:0;">
              <ul data-role="listview" data-inset="true" data-theme="c" data-dividertheme="a">
                  <li data-role="list-divider">Options</li>
                  <li><a href="#view_map" data-transition="pop">View tracks near you</a></li>
			<!-- ... more links etc. ... -->
              </ul>
          </div>
      </div>
      <div data-role="page" id="view_map" style="width:100%; height:100%;">
          <div data-role="header" data-position="fixed" id="view_map_header">
              <a data-rel="back">Back</a>
              <h2>Tracks near you</h2>
          </div>
          <div data-role="content" style="width:100%; height:100%; padding:0;">
              <div id="map_tracks"></div>
          </div>
      </div>
<!-- ... more pages ... -->
  </body>

Basic JQuery Mobile Mulitpage for the App (source)

In the partial source code of the index.html you can easily see, that we have two pages, identified by data-role="page"-Attribute and each contains a header (data-role="header") and a div for the content (data-role="content"). On our main page with the id="home" we have just a list of links to the other pages. To link to a page, you just need a basic link where the href is a # followed by the id of the page (see lines 7 and 12). If you want to have a special animation for the transition you can simply add a data-transition-Attribute.

So now we have the basic html with some links and pages, but now we have to call the rest-service an get the stuff we want to display on the pages.
To prepare the content of a page JQuery Mobile offers some events you can listen to. For our use case the page transition events (pagebeforeshow, pageshow, pagehide) and the page creation events (pagecreate and pageinit) are the most important. You should consider carefully what you do on each of the events (we have not done this carefully, so our code might not be perfect :-) ).

For our page where we display the tracks near you on a map this looks basically like this:

$("#view_map").live("pageinit", function() {
    initMapSize("#map_tracks");
});

$("#view_map").live("pageshow", function() {
    mapMarkers = [];
    var lat = 48.128569; // default location: cS HQ
    var lon = 11.557289;
    if(navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(function(position){
            lat = position.coords.latitude;
            lon = position.coords.longitude;
            initTracksPage(lat, lon);
        });
    } else {
        initTracksPage(lat, lon);
    }
});

If the page is first initialized the pageinit event ist triggered by JQueryMobile, if this happens we prepare our map and make sure it has the right size for the device. If the page is shown to the user (Event: pageshow) we finally make sure we get the current location of the user (if the browser supports this) and load the tracks to show on the map. To call our rest-service we simply use the well known $.getJSON():

var path = '../services/track/get/' + lon + '/' + lat + '/' + maxdistance + '/';
$.getJSON(path, function(data) {
    $.each(data, function(idx, track) {
        addMarker(track);
    });
});

The other parts of the app are build the same way, so simply get the code from GitHub and have a look around.



JQuery Mobile is a basic and easy to use framework to create a webapp focused on mobile devices. Other frameworks like Sencha Touch may offer some more
functionality. But I guess next time we should simply use a responsive design approach.



Published at DZone with permission of Daniel Bartl, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)