One of the things that continues to amaze me about the Groovy language is how easy it is to accomplish seemingly complex tasks in a very short amount of time. Recently, I've been working on a Grails web application that stores a lot of geospatial information that I am interested in plotting on a map.
In order to accomplish this task I decided to use Google's Map API because 1) it's free and 2) Google Maps are awesome.
Note: Unless you are already familiar with the Google Maps API, it would probably be beneficial to start with this blog entry: A Brief Introduction to Google Maps.
Let's pretend that the application I am creating stores the location of various water parks around the country. In most cases this information would be stored into a database, but for the sake of this example let's also pretend that I'm storing this information in a comma separated values file that looks like this:
Mountain Creek Waterpark,200 Route 94,Vernon,NJ,07462
Aquaport,2344 McKelvey Rd, Maryland Heights,MO,63043
Knott's Soak City,1500 S Gene Autry Trail,Palm Springs,CA,92264
Adventure Landing,1944 Beach Blvd,Jacksonville Beach,FL,32250
As we can see in my post, A Brief Introduction to Google Maps, plotting geographic information using Google Maps is quite simple; however, there is a significant performance hit when using this technique. Every time we go to plot a new map location we are calling the getLatLng() provided by the Google Maps API:
geocoder = new GClientGeocoder();
if (geocoder) {
geocoder.getLatLng(
address,
function(point) {
var marker = new GMarker(point);
map.addOverlay(marker);
map.addControl(new GLargeMapControl());
}
);
}
There are a couple of problems with this approach:
- It's slow. You have to make a call to Google's Geocoding service for each address you are looking up. In addition, Google sets a maximum query rate to prevent users from trying to geocode too many addresses at once. In order to prevent this from happening, it's common to code a delay into your software, which will also obviously slow down the speed of your application.
- Google limits you to 15,000 geocode lookups per day. If you have a site that receives a decent amount of traffic, it's not inconceivable that you could easily reach this limit.
What we need is a way to avoid calling the Geocode Service in JavaScript each time we want to plot an item on a map. A great way to handle this is to take care all of the geocoding needs upfront. Fortunately, Google provides a service to allow us to do just that.
In order to get the latitude and longitude coordinates for an address, we simply need to build a URL with the following query parameters:
- q - The address that you want to geocode.
- key - Your Google Maps API key.
- output - The format in which the output should be generated. The options are xml, kml, csv, or json.
For example, if we wanted to geocode the following address:
549 East Rochambeau Drive
Williamsburg, VA 23188
we would simply create the following url:
http://maps.google.com/maps/geo?
q=549 East Rochambeau Drive+Williamsburg+VA+23188
&output=xml&key=YOUR_GOOGLE_MAPS_API_KEY_HERE
If you copy and paste this URL into a browser you will receive the following XML response:
<kml xmlns="http://earth.google.com/kml/2.0">
<Response>
<name>
549 East Rochambeau Drive+Williamsburg+VA+23188
</name>
<Status>
<code>200</code>
<request>geocode</request>
</Status>
<Placemark id="p1">
<address>
E Rochambeau Dr, Williamsburg, VA 23188, USA
</address>
<AddressDetails Accuracy="6"
xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0">
<Country>
<CountryNameCode>US</CountryNameCode>
<AdministrativeArea>
<AdministrativeAreaName>
VA
</AdministrativeAreaName>
<SubAdministrativeArea>
<SubAdministrativeAreaName>
York
</SubAdministrativeAreaName>
<Locality>
<LocalityName>
Williamsburg
</LocalityName>
<Thoroughfare>
<ThoroughfareName>
E Rochambeau Dr
</ThoroughfareName>
</Thoroughfare>
<PostalCode>
<PostalCodeNumber>
23188
</PostalCodeNumber>
</PostalCode>
</Locality>
</SubAdministrativeArea>
</AdministrativeArea>
</Country>
</AddressDetails>
<Point>
<coordinates>-76.741136,37.342917,0</coordinates>
</Point>
</Placemark>
</Response>
</kml>
As we can see the latitude and longitude coordinates are defined in the following XML snippet:
<Point>
<coordinates>-76.741136,37.342917,0</coordinates>
</Point>
That's great, but we obviously don't want to create a URL by hand and copy and paste the coordinates out of the XML response. This is where Groovy comes in. As mentioned earlier, we have a text file of water park locations that we are interested in geocoding. To accomplish this task we'll use the following code:
new File('water_park_locations.txt').eachLine { lineText ->
waterPark = lineText.tokenize(',')
waterParkName = waterPark[0]
address = waterPark[1]
city = waterPark[2]
state = waterPark[3]
zip = waterPark[4]
returnedXML = geocode(address, city, state, zip)
slurpedXML = new XmlSlurper().parseText(returnedXML)
statusCode = slurpedXML.Response.Status.code
if(statusCode == 200){ // make sure everything's OK
coordinates = slurpedXML.Response.Placemark.Point.coordinates
coordinates = coordinates.toString().tokenize(',')
latitude = coordinates[1]
longitude = coordinates[0]
println "${waterParkName} coordinates: " +
"${latitude}, ${longitude}"
}
Thread.sleep(250) //don't go too fast
}
The first few lines of code simply read the file line by line and parse out the individual pieces of water park information we are interested in (i.e. name, street address, city, state, and zip). Once all of these values are gathered they are passed as parameters to the geocode() method, which contains the following code:
def geocode (address, city, state, zip) {
key = 'YOUR_GOOGLE_MAPS_API_KEY_HERE'
query = "${address}+${city}+${state}+${zip}"
output = "xml"
baseURL = 'http://maps.google.com/maps/geo'
url = "${baseURL}?q=${URLEncoder.encode(query)}"
url += "&output=${output}&key=${key}"
results = new URL(url).text
return results;
}
This method simply takes in the water park address information in as parameters, creates a url that meets the Google Geocoding services format, calls the URL, and collects and returns the response XML.
Once the XML response is returned we need to parse out the latitude and longitude coordinate information. Ordinarily this would be a pretty painful task using a language like Java, but Groovy makes it easy by providing the "XmlSlurper" class.
All we need to do is create an XmlSluprer object and pass the XML returned from Google into the XmlSpurper's parseText() method.
slurpedXML = new XmlSlurper().parseText(returnedXML)
One piece of information that we will be interested in extracting from the XML response is the "status code" returned by the Google Geocoding service. In order to extract the status code we simply walk the XML document tree and collect the values using the following Groovy code:
statusCode = slurpedXML.Response.Status.code
Notice how the slurped XML can be accessed simply by using the element names of the XML returned by the Google Geocoding service.
<Response>
<name>
549 East Rochambeau Drive+Williamsburg+VA+23188
</name>
<Status>
<code>200</code>
<request>geocode</request>
</Status>
...
</Response>
The status code returned by Google will let us know whether or not the address we sent to the service was successfully geocoded. A status code of "200" means everything went well. Click here for a full list of status codes.
Once the status code checks out, we can extract the latitude and longitude information out of the returned XML and print it to the screen by using the following code:
coordinates = slurpedXML.Response.Placemark.Point.coordinates
coordinates = coordinates.toString().tokenize(',')
latitude = coordinates[1]
longitude = coordinates[0]
println "${waterParkName} coordinates: " +
"${latitude}, ${longitude}"
Finally we tell the code to sleep for a ¼ of a second by calling the Thread.sleep() method:
Thread.sleep(250)
This will prevent us from calling Google's Geocoding service too fast and having it fail on us.
Once we execute all of the code we should receive the following output:
Mountain Creek Waterpark coordinates: 41.191648, -74.509150
Aquaport coordinates: 38.725388, -90.446879
Knott's Soak City coordinates: 33.804852, -116.493079
Adventure Landing coordinates: 30.288094, -81.412461
It would probably be a good idea to store the latitude and longitude coordinates into a database and then whenever you wanted to plot items to a map all you would need to do is select these values from the database and skip the geocoding step. This approach offers a huge performance advantage and also helps prevent the 15,000 geocode service calls limit imposed by Google.
Resources
To download the source for this example, click here.
Comments
Post new comment