Thursday 17 May 2012

Spatial data with Grails and MongoDB


Recently, I have been making a presentation during TomTom Dev Day about "Rapid web development using Groovy and Grails". Key point of my presentation was live development of simple POI (Point of Interest - for example: bank, restaurant etc) administration system. User has an web interface, where he can add, remove and list places and data is stored in MongoDB to provide spatial indexing of points. To illustrate spatial quering application implements REST webservice that finds POIs near given point. I am going to simplify the project a bit, but it should be enough to get how those technologies are working together.

Project configuration
To start you will need:
- download Grails (I used 2.0.3)
- download MongoDB (I used 2.0.4)
- Spring Tool Suite is optional

Firstly, create a new Grails project. Next step is to configure it to use MongoDB instead of Hibernate in GORM. Let's go to project directory and reconfigure plugins:

grails uninstall-plugin hibernate
grails install-plugin mongodb 1.0.0.RC4

If you are using STS, refresh the project as well as run "Grails Tools - Refresh Dependencies". I figured out that we need also to update configuration file conf/BuildConfig.groovy:

//runtime ":hibernate:$grailsVersion"

I am not sure, but maybe it should be already done by uninstall-plugin command, so possibly it is a bug in Grails. 

Writing the code
Now, let's add domain object - foo.MyPoi:

package foo

class MyPoi {
    String name
    Double longitude
    Double latitude
    List location
    static constraints = { name(blank: false) }
    static mapping = { location geoIndex:true }
}

MyPoi has its name and lon/lat coordinates as well as strictly technical field named "location". MongoDB cannot understand coords in separate Double fields like longitude and latitude - it need special list containg these values. Please also look at the definition of geoIndex - hint for MongoDB to create spatial index.



Now, it's time to add controller foo.MyPoiController. We will use scaffolding to make things easy. We will only need to override save method to update location field based on longitude and latitude (location field as List will be skipped by scaffolding).

package foo

import grails.converters.JSON

class MyPoiController {
    static scaffold = MyPoi

    def save() {
        def myPoi = new MyPoi(params)
        myPoi.location = [
            myPoi.latitude,
            myPoi.longitude
        ]
        myPoi.save()
        redirect(action: 'list')
    }
    def listNearPointAsJson() {
        def point = [
            new Double(params.lat),
            new Double(params.lon)
        ]

        def myPois = MyPoi.findAllByLocationWithinCircle([point, new Double(params.r)])
        render myPois as JSON
    }
}

I have added custom listNearPointAsJson controller action to implement webservice. Notice the beauty of autogenerated MyPoi.findAllByLocationWithinCircle method that allows us to find POIs :)

NOTE: I discovered that Grails is generating HTML5 forms in scaffolded views. Moreover, Double fields are generated as "number" input type. It seems that Chrome has a bug related to validation of values entered in such fields: http://stackoverflow.com/questions/8052962/groovy-grails-float-value-and-html5-number-input - please use Firefox for now. Bug should be corrected soon, but you can also make it work with static scaffolding (generate view gsp files) and make simple correction describe in linked article.


Running our application
Start MongoDB with command like:
mongod.exe --dbpath ../data --rest

Start our Grails project:
grails run-app

Add some places using web interface:













And here is the list:












Let's play with WS to check if spatial indexing works. At first let's get a query for the point that is far away from data we just entered: http://localhost:8080/PoiTest6/myPoi/listNearPointAsJson?lat=51.0&lon=18.0&r=0.5

Result is empty JSON:
[]

Now, let's change the query point to get one of POIs inside radius defined as third url param: http://localhost:8080/PoiTest6/myPoi/listNearPointAsJson?lat=51.0&lon=18.7&r=0.5

Result is as expected:







We can move it closer to second POI in DB: http://localhost:8080/PoiTest6/myPoi/listNearPointAsJson?lat=51.0&lon=19.0&r=0.5 to get both POIs:

[{"class":"foo.MyPoi","id":1,"latitude":51.1,"longitude":19.1,"name":"poi1"},{"class":"foo.MyPoi","id":2,"latitude":51.2,"longitude":19.2,"name":"poi2"}]

Summary
It's all very simple as it is usually with Grails. Integration with MongoDB is also straightforward. Unfortunatelly, there was also a little surprise with Chrome... Anyway, I hope it was interesting.

4 comments:

  1. how can i link an address with a geolocation and search either by zipcode / geocode / address and show on google map?

    ReplyDelete
    Replies
    1. Integration on view side with Google Map is very easy. Just override scaffolded show.gsp with you own implementation that uses Google Maps API and its done. Searching by zipcode and geocode and address is much more complicated and would require more complex data model and implementation.

      Delete
    2. can u pls show the pseudocode too? thanks

      Delete
  2. My 5 cents...
    You don't have to uninstall the Hibernate plugin in order to work with Mongo.
    You can add static mapWith = "mongo" to the classes which you want to persist
    in Mongo.

    ReplyDelete