Moving My Google Maps Mashup to Grails, part two

I made a few improvements to my Google Maps mashup, and though they’re not as significant as the changes made in my previous posts, I still wanted to make a record of them.

I discussed in my last post the problem I had exposing my domain objects as JSON strings. To summarize the problem:

  1. In order to use the Google Maps API, I need to be able to pass latitude and longitude information from my Location objects (written in Groovy) into JavaScript. Grails pulls the data from a database and populates all the Locations, which are passed from the controller to the view. Somehow the JavaScript in the view needs to extract the coordinates in order to draw the markers on the map.
  2. The page where I want to do the rendering actually lists courses, not just locations. My Course class has an attribute of type Location, which is fine, but means I need to traverse a relationship in order to access the data I need. Normally that wouldn’t be an issue, but JavaScript doesn’t know how to do that.
  3. I can use the convenient Grails “converters” plugin to translate my Course objects into JSON, but that leads to difficulties as well. It seems that the converter doesn’t follow relationships, or, rather that it just inserts the foreign key value where the relationship goes. In other words, the JSON version of a particular Course only lists an integer in the location field.
  4. As a result of (3), I decided to pass both the collection of Course objects and the collection of Location objects to the view. That in itself was awkward, because it meant I had to “dereference” the location array myself for each course, which felt highly brittle.
  5. Also, when the converter operated, it dumped all the information about my objects directly into the web page, where it could be read by anyone who knows how to do a “view source” command. Not good.

The way I solved the problem was to create a special class in my Grails project called CourseMapData:


class CourseMapData {
    String title
    Date startDate
    Date endDate
    String city
    String state
    double latitude
    double longitude
}

That class encapsulates all the data I need out of both the Course object and its associated location. It also leaves out any attributes that might possibly be sensitive. Since it doesn’t have any associations to any other classes, I can transform it into JSON without losing any information.

Since it’s not actually a domain class, I put the source file in the src/groovy folder of my Grails project. In my CourseController, I then added the necessary translation:


def list = {
    if(!params.max)params.max = 10
    def courseList = Course.list(params)
    def cmList = []
    courseList.each { c ->
        def cm = new CourseMapData()
        cm.title = c.title
        cm.startDate = c.startDate
        cm.endDate = c.endDate
        cm.city = c.location.city
        cm.state = c.location.state
        cm.latitude = c.location.latitude
        cm.longitude = c.location.longitude
        cmList << cm
    }

    [ courseList:courseList, cmList:cmList]
}

I was hoping their might be a more elegant way to populate the object (something similar to how request parameters are passed into objects using “obj.properties = params“), but it was easy enough to just write them out. That’s especially true because the values come from both Course and Location.

By passing the collection of CourseDataObjects, my JavaScript code in my list.gsp page reduces to:


function initialize() {
    map = new google.maps.Map2($("map"));
    var homeJSON = ${Location.findByCityAndState('Marlborough','CT').encodeAsJSON()};
    home = new google.maps.LatLng(homeJSON.latitude,homeJSON.longitude);
    map.setCenter(home, 4);

    var cmsJSON = ${[cms:cmList].encodeAsJSON()};
    var courseDataArray = cmsJSON.cms;

    processCourses(courseDataArray);

    map.enableScrollWheelZoom();
    map.addControl(new google.maps.LargeMapControl());
    map.addControl(new google.maps.MapTypeControl());
    map.addControl(new google.maps.OverviewMapControl());
}
google.setOnLoadCallback(initialize);

Now I’m only converting one collection (cmList), and although it dumps a lot of code in the page source, nothing in it is particularly sensitive. The rest is essentially the same as before.

I’m still not quite ready to put my application online, because it hasn’t been secured yet. I plan to use a beforeInterceptor to separate the admin pages from the public pages. It’s a little awkward, though, because this isn’t a site that I actually want anyone but myself to modify. Still, I’d like to be able to access it myself when I’m traveling in order to add in new courses and/or locations.

Hopefully I’ll get a chance to add that functionality soon.

As an aside, I fixed another issue that occurred to me. Obviously I don’t want to have to enter a latitude and a longitude when I create a new Location. I have an earlier post here that talks about using the Google geocoder service to do the translation for me. I finally got around to adding that functionality to my Grails application.

Now the save method in my LocationController looks like:


def save = {
    def location = new Location()
    location.properties = params
    if (location.latitude == 0 && location.longitude == 0) {
        // Use the Google Maps geocoder to fill in latitude and longitude
        def key = 'longUglyGoogleMapsKey'
        def base = 'http://maps.google.com/maps/geo'
        def query = "q=${location.city},+${location.state}&output=csv&key=${key}"
        def url_string = base + '?q=' +
            ',+' + URLEncoder.encode(location.city,"UTF-8") +
            ',+' + URLEncoder.encode(location.state,"UTF-8") + 
           "&output=csv&key=${key}"
        def results = new URL(url_string).text.split(',')
        location.latitude = results[-2].toDouble();
        location.longitude = results[-1].toDouble();
    }
    if(location.save()) {
        flash.message = "Location ${location.city}, ${location.state} created."
        redirect(action:show,id:location.id)
    } else {
        render(view:'create',model:[location:location])
    }
}

To use it, I modified my create.gsp page for Locations to include the phrase:

“Note: leave latitude and longitude empty or 0.0 to use the embedded geocoder.”

Then when I enter the data, I can see the latitude and longitude fill in automatically. Sweet.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.