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:
- 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 theLocations
, 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. - The page where I want to do the rendering actually lists courses, not just locations. My
Course
class has an attribute of typeLocation
, 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. - 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 particularCourse
only lists an integer in thelocation
field. - As a result of (3), I decided to pass both the collection of
Course
objects and the collection ofLocation
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. - 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