I’ve been playing around with Grails and a bit of GWT lately, and it sure is a lot of fun. I’ve made some applications that use Grails, GWT, and GoogleMaps (see: Find A Wii Fit), and thought I’d write a quick tutorial on how to do it - it can be a little tricky.

This application is fairly simple - it displays a google map view that is centered on the user’s location, and inserts a marker where they are located. I am using MaxMind to turn an IP into latitude and longitude coordinates. They cost money and you may be able to find a free one, but they aren’t that expensive (something like $20 for 50,000 lookups) and I’ve been pretty happy with them so far.

There is a GWT+Grails plugin that makes it easy for Grails and GWT to work together. I am also using GWT-EXT to make it look nice, and to make the GoogleMaps integration a little easier. If you want to see this app in action, you can see it here (it may not match the example exactly - I’m actively developing it). I’ve also made the entire project available for free, here.

1.Create a new Grails application (grails create-app findme)

2.Install the Grails + GWT plugin (grails install-plugin gwt)

3.Create your Grails Module (grails create-gwt-module net.uresk.FindMe)

4. Create your page (grails create-gwt-page main/index.gsp net.uresk.FindMe, answer yes when it asks if you want it to create the controller)

5. Download GWT-EXT (http://gwt-ext.com/download/), put gwtext.jar in your project’s lib/gwt directory (ie, $PROJECT_NAME/lib/gwt). You’ll also have to put it into the plugins/gwt-0.2.4/lib directory.

6. We’ll then have to hack _Internal in the /plugins/gwt-0.2.4/scripts/ directory (hopefully this won’t be necessary in the next version of the plugin). Around line 89, you’ll have to add in another include entry after the others:

Include(name: ‘gwtext.jar’);

We’ll also need to go down a few more lines, and before ‘pathElement(location: “${basedir}/${srcDir}”)’, add ‘pathElement(location: “${basedir}/lib/gwt/gwtext.jar”)’.

Lastly, if you get an error about heap space, you’ll need to give the gwt compiler more heap space. Do this by adding in ‘jvmarg(value: ‘-Xmx256M’)’ a few lines down (around line 102), just above where the other arg() lines start. Not sure if this is related to using gwt-ext, running on Mac OS X, or what, but I did run into it a few times.

7. Download EXT 2.0.2 (http://yogurtearl.com/ext-2.0.2.zip), copy the ‘adapter’ and ‘resources’ directory, along with the ‘ext-all.js’, ‘ext-all-debug.js’, ‘ext-core.js’, and ‘ext-core-debug.js’ into your project’s /web-app/js/ directory.

8. Download mapstraction (http://mapstraction.com/svn/source/mapstraction.js) and copy it into your applications web-app/js directory as well.

9. Open up your Grails project (I prefer IntelliJ for Groovy/Grails development at the moment, btw) and find the FindMe.gwt.xml file (it is in /src/java/net/uresk directory). Add in the following:

<inherits name=“com.gwtext.GwtExt” /> <stylesheet src=“/findme/js/resources/css/ext-all.css” /> <script src=“/findme/js/adapter/ext/ext-base.js” /> <script src=“/findme/js/ext-all.js” />

10. Add the following entries in:
Create your Google Maps keys for localhost and for your production domain. To make things easy, I added a config property in called ‘googlemaps.key’, and then added this into my controller to get it for the right environment:[key: grailsApplication.config.googlemaps.key]12. In your index.gsp, add in the following:

A reference to mapstraction: <script type=”text/javascript” src=”/findme/js/map/mapstraction.js”></script>

The google maps js include: <script type=”text/javascript” src=”http://maps.google.com/maps?file=api&v=2.x&key=${key}”></script>

13. Now create a service called something like LookupService, and add in the following code to access MaxMind:

class LookupService {         static expose = [‘gwt:net.uresk.client’]         Double[] getLatLonFromIp(String ip){                 if(!(ip ==~ /bd{1,3}.d{1,3}.d{1,3}.d{1,3}b/)){                         ip = defaultIp                 }                 def res = “http://geoip1.maxmind.com/b?l=${maxMindKey}&i=${ip}”.toURL().text.split(“,”)                 Double[] result = new Double[2]                 result[0] = Double.parseDouble(res[3])                 result[1] = Double.parseDouble(res[4])                 return result         } }

You can swap out MaxMind with any other service. I added a maxMindKey config property in Config.groovy, but you can easily hard-code it.

14. Now create your GWT client code, something like this:

GoogleMap mainPanel;     /**      * This is the entry point method.      */     public void onModuleLoad() {                 createPanel();             new Viewport(mainPanel);             updateLatLonFromMaxMind();     }         private void updateLatLonFromMaxMind(){                 LookupServiceAsync myService = (LookupServiceAsync) GWT.create(LookupService.class);                 ServiceDefTarget endpoint = (ServiceDefTarget) myService;                 String moduleRelativeURL = GWT.getModuleBaseURL() + “rpc”;                 endpoint.setServiceEntryPoint(moduleRelativeURL);                 AsyncCallback callback = new AsyncCallback() {                   public void onSuccess(Object result) {                     Double[] latLon = (Double[])result;                         setLatLong(latLon[0], latLon[1]);                   }                   public void onFailure(Throwable caught) {                     // do some UI stuff to show failure                   }                 };                 myService.getLatLonFromIp(callback);         }         private void setLatLong(Double lat, Double lon){                 LatLonPoint llp = new LatLonPoint(lat.doubleValue(), lon.doubleValue());                 Marker m = new Marker(llp);                 m.setInfoBubble(“<div>You are here!</div>”);                 mainPanel.setCenterAndZoom(llp, 10);                 mainPanel.addMarker(m);         }         private void createPanel(){                 mainPanel = new GoogleMap();                 mainPanel.setTitle(“Spencer’s Google Map Page”);                 mainPanel.addLargeControls();         }

15. Now, we need to create the stubs for this service so that GWT can use it. Do this by running ‘grails run-app’.

16. Unfortunately, there currently isn’t an easy way to get at the HttpRequest object inside our service. There is a JIRA item open for this, and I’m working on a potential solution. In the meantime, we’ll have to do a bit of hacking.

First, we’ll need to open up the LookupService and LookupServiceAsync java files that the plugin generated – they are in /src/net/uresk/client in this example. Modify the method signatures so they don’t take a String object (the IP). They should look like: java.lang.Double[] getLatLonFromIp(); and void getLatLonFromIp(AsyncCallback callback);

Next, we’ll need to modify the GrailsRemoteServiceServlet (in /plugins/gwt-0.2.4/src/groovy/org/codehaus/groovy/grails/plugins/gwt). On line 53, we are going to modify the method call so that it explicitly passes in the ip address, by changing it to say this:

def retval = service.invokeMethod(serviceMethod.name, this.getThreadLocalRequest().getRemoteAddr())

If you are using this behind a proxy (ie, behind Apache HTTPD with mod_proxy), getRemoteAddr() won’t work – you’ll have to instead get the X-Forwarded-For header value.

This is a nasty hack, and will break all your other GWT RPC calls, so if you are going to do more with this example, you’ll have to do something a little smarter to inject the IP (contact me if you want some ideas, and also keep an eye on the JIRA item mentioned above). Hopefully it won’t be necessary much longer.

You should be able to run your application now! Do ‘grails run-app’ again and browse to http://localhost:8080/findme/main.

Let me know if you have any questions or suggestions. I also hang out on the #groovy IRC channel on the evenings and weekends (GMT -700) if you need help.

Have fun :)

Special Thanks to Abhijeet Maharana who provided an excellent how-to for using GoogleMaps with Gwt-Ext.

Update: Sorry, I forgot to include the GWT client code in the original post. It is in there, step 14 now. Let me know if I missed anything else.