Monday, April 6, 2009

Google Geocoding and Restlet

The other week I needed to code a web page containing interaction between a set of elements and Google Maps, driven off a list of html links in a separate section of the page . The elements had latitude and longitude fields in the database, but this information wasn't completely filled in, especially for internationally based elements.

Given that I needed to be able to click on the links and bring back an individual element (or a subset of elements), I pulled the data out of the database and shoved it in a cache, rather than looking up the data each time (basic stuff).

The problem was, I had about a quarter of the elements lacking the necessary coordinate information to plot on the map.

So the idea was to fill in as much as the missing information at cache load, using Google's Gecoding service. To call the Google Gecoding API, I used the Restlet framework. Code is below, note that not all classes are included (such as model objects and constants files). The cache implementation is Ehcache.

All code stripped down to protect the innocent.

Method to load cache:

public synchronized void load()
{
PropertyDAO propDao = new PropertyDAOImpl(conn);
List<Property> propList = null;

try
{
// get the properties from growth right now
propList = propDao.getPropertiesGeoInfo();
}
catch (SQLException e)
{
e.printStackTrace();
}

if (propList != null)
{
// get the property cache
CacheManager manager = CacheManager.getInstance();
Cache propCache = manager.getCache("myCache");

// for each property, check if it has latitude and longitude, fill in if it doesn't and put in the list
for (Property prop : propList)
{
// check to see if there is a latitude and longitude
if (prop.getLatitudeNbr() == 0 || prop.getLongitudeNbr() == 0)
{
lookupGeoInfo(prop);
}

// add it to the cache
propCache.put(new Element(prop.getFacilityNbr(), prop));
}
}
}
The Property object is the pojo representing the element to be mapped. It comes from the data source with latitude and longitude information, but if it's empty, that's where the Geocoding service comes in:

private void lookupGeoInfo(Property prop)
{
// create a new restlet request
Request request = new Request();
Reference ref = new Reference("http://maps.google.com/maps/geo");

// map the parameters to the geocoding service
mapParams(ref, prop);

request.setResourceRef(ref);
request.setMethod(Method.GET);

Client client = new Client(Protocol.HTTP);

// call Google and get the response back
Response response = client.handle(request);

if (response != null)
{
Representation rep = response.getEntity();

try
{
/***
* split the tokens of the response.
* The format is 4 tokens:
* 200,6,42.7,-73.69
* Status code, accuracy, latitude, longitude
*
**/
String[] tokens = rep.getText().split(",");
// if the response code is success, get the lat and long and set it on the prop object
if ("200".equalsIgnoreCase(tokens[0]))
{
prop.setLatitudeNbr(Double.parseDouble(tokens[2]));
prop.setLongitudeNbr(Double.parseDouble(tokens[3]));
}
}
catch (IOException ie)
{
ie.printStackTrace();
}
}

// explicitly close the call
request.release();
}
Here we create a Restlet Request and set it's endpoint to Google's Geocoding endpoint. Once we've established where the request is going, we set the parameters Google expects (handled by mapParams). Note the format of the response - in this case, I just want the latitude and longitude, so I request the response to come back as csv format and parse it according to it's positions. There are other formats available, check the Geocoding Responses section for more info.

mapParams method:

private void mapParams(Reference ref, Property prop)
{
// flag to know whether to add a comma or not
boolean first = true;

StringBuilder sb = new StringBuilder();
// add address line one if it's there
if (!nullOrEmpty(prop.getAddrLn1Txt()))
first = formatForParam(prop.getAddrLn1Txt(), first, sb);

if (!nullOrEmpty(prop.getAddrLn2Txt()))
first = formatForParam(prop.getAddrLn2Txt(), first, sb);

if (!nullOrEmpty(prop.getCityName()))
formatForParam(prop.getCityName(), first, sb);

if (!nullOrEmpty(prop.getStateCd()))
formatForParam(prop.getStateCd(), first, sb);

if (!nullOrEmpty(prop.getPostalCd()))
formatForParam(prop.getPostalCd(), first, sb);

if (!nullOrEmpty(prop.getCountryName()))
formatForParam(prop.getCountryName(), first, sb);

// now the address parameter built from the prop object
ref.addQueryParameter("q", sb.toString());

// add the sensor param (true for a gps device)
ref.addQueryParameter("sensor", "false");

// the desired output - all we need is the csv, we don't care about other data they send back
ref.addQueryParameter("output", "csv");

// the encoding the response is coming back in
ref.addQueryParameter("oe", "utf8");

// add the key to geo code request
ref.addQueryParameter("key", geoCodeKey);
}
The Reference.addQueryParameter method adds a query string parameter to your outgoing Restlet Request, allowing you to set to set parameters more elegantly than building the whole url in a StringBuilder/StringBuffer. The method attempts to build the address line off of the Property object fields, and then sets all other parameters for the geocoding service appropriately. Note that you'll need a key from Google to perform this call, same as you would with Google Maps.

Putting it all together:

public class PropertyCacheLoad
{
private Connection conn = null;
private String geoCodeKey = null;

public PropertyCacheLoad(Connection conn, String geoCodeKey)
{
this.conn = conn;
this.geoCodeKey = geoCodeKey;
}

/*
*
*/
public void configure(String configFile)
{
CacheManager.create(configFile);
}

/*
*
*/
public synchronized void load()
{
PropertyDAO propDao = new PropertyDAOImpl(conn);

List<property:gt; propList = null;

try
{
// get the properties from growth right now
propList = propDao.getPropertiesGeoInfo();
}
catch (SQLException e)
{
e.printStackTrace();
}

if (propList != null)
{
// get the property cache
CacheManager manager = CacheManager.getInstance();
Cache propCache = manager.getCache("myCache");

// for each property, check if it has latitude and longitude, fill in if it doesn't and put in the list
for (Property prop : propList)
{
// check to see if there is a latitude and longitude
if (prop.getLatitudeNbr() == 0 || prop.getLongitudeNbr() == 0)
{
lookupGeoInfo(prop);
}

// add it to the cache
propCache.put(new Element(prop.getFacilityNbr(), prop));
}
}

}

/**
* @param prop
*/
private void lookupGeoInfo(Property prop)
{
// create a new restlet request
Request request = new Request();
Reference ref = new Reference("http://maps.google.com/maps/geo");

// map the parameters to the geocoding service
mapParams(ref, prop);

request.setResourceRef(ref);
request.setMethod(Method.GET);

Client client = new Client(Protocol.HTTP);

// call Google and get the response back
Response response = client.handle(request);

if (response != null)
{
Representation rep = response.getEntity();

try
{
/***
* split the tokens of the response.
* The format is 4 tokens:
* 200,6,42.7,-73.69
* Status code, accuracy, latitude, longitude
*
**/
String[] tokens = rep.getText().split(",");
// if the response code is success, get the lat and long and set it on the prop object
if ("200".equalsIgnoreCase(tokens[0]))
{
prop.setLatitudeNbr(Double.parseDouble(tokens[2]));
prop.setLongitudeNbr(Double.parseDouble(tokens[3]));
}
}
catch (IOException ie)
{
ie.printStackTrace();
}
}

// explicitly close the call
request.release();
}

/**
* @param ref
* @param prop
*/
private void mapParams(Reference ref, Property prop)
{
// flag to know whether to add a comma or not
boolean first = true;

StringBuilder sb = new StringBuilder();
if (!nullOrEmpty(prop.getAddrLn1Txt()))
first = formatForParam(prop.getAddrLn1Txt(), first, sb);

if (!nullOrEmpty(prop.getAddrLn2Txt()))
first = formatForParam(prop.getAddrLn2Txt(), first, sb);

if (!nullOrEmpty(prop.getCityName()))
formatForParam(prop.getCityName(), first, sb);

if (!nullOrEmpty(prop.getStateCd()))
formatForParam(prop.getStateCd(), first, sb);

if (!nullOrEmpty(prop.getPostalCd()))
formatForParam(prop.getPostalCd(), first, sb);

if (!nullOrEmpty(prop.getCountryName()))
formatForParam(prop.getCountryName(), first, sb);

// now the address parameter built from the prop object
ref.addQueryParameter("q", sb.toString());

// add the sensor param (true for a gps device)
ref.addQueryParameter("sensor", "false");

// the desired output - all we need is the csv, we don't care about other data they send back
ref.addQueryParameter("output", "csv");

// the encoding the response is coming back in
ref.addQueryParameter("oe", "utf8");

// add the key to geo code request
ref.addQueryParameter("key", geoCodeKey);
}

/**
* @param s
* @return
*/
private boolean nullOrEmpty(String s)
{
return (s == null || s.trim().length() == 0);
}

/**
* @param s
* @param first
* @param sb
* @return
*/
private boolean formatForParam(String s, boolean first, StringBuilder sb)
{
if (first)
sb.append(s);
else
sb.append(", " + s);

return false;
}

}

No comments:

Post a Comment