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"}]
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.