Sunday, August 26, 2007

Lightweight Google Geocoder with Java

I often look for tools before deciding to build them myself for obvious reasons: saving time and labor. I was looking for a Java implementation of Google geocoder but there seems to be only this one GeoGoogle. My first impression is that the tool is a little heavy for its job. Further more, it chooses to deal with XML format and all the parsing and schema validation is a bit too much for my taste. To use Google geocode service, all you need is to send an HTTP request along with your address. The response will be in either XML or JSON format. It will include details about the address you sent such as city, state, zipcode, etc, but the most important things are longitude and latitude. So I decided to go with JSON and wrote this tool. Hopefully, someone else might find it useful. Many thanks to the folks that wrote JSON-LIB.


public class GGcoder {
private static final String URL = "http://maps.google.com/maps/geo?output=json";
private static final String DEFAULT_KEY = "YOUR_GOOGLE_API_KEY";

public static GAddress geocode(String address, String key) throws Exception {
URL url = new URL(URL + "&q=" + URLEncoder.encode(address, "UTF-8")
+ "&key=" + key);
URLConnection conn = url.openConnection();
ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
IOUtils.copy(conn.getInputStream(), output);
output.close();

GAddress gaddr = new GAddress();
JSONObject json = JSONObject.fromString(output.toString());
JSONObject placemark = (JSONObject) query(json, "Placemark[0]");

final String commonId = "AddressDetails.Country.AdministrativeArea";

gaddr.setFullAddress(query(placemark, "address").toString());
gaddr.setZipCode(query(placemark,
commonId + ".SubAdministrativeArea.Locality.PostalCode.PostalCodeNumber")
.toString());
gaddr.setAddress(query(placemark,
commonId + ".SubAdministrativeArea.Locality.Thoroughfare.ThoroughfareName")
.toString());
gaddr.setCity(query(placemark,
commonId + ".SubAdministrativeArea.SubAdministrativeAreaName").toString());
gaddr.setState(query(placemark, commonId + ".AdministrativeAreaName").toString());
gaddr.setLat(Double.parseDouble(query(placemark, "Point.coordinates[1]")
.toString()));
gaddr.setLng(Double.parseDouble(query(placemark, "Point.coordinates[0]")
.toString()));
return gaddr;
}

public static GAddress geocode(String address) throws Exception {
return geocode(address, DEFAULT_KEY);
}

/* allow query for json nested objects, ie. Placemark[0].address */
private static Object query(JSONObject jo, String query) {
try {
String[] keys = query.split("\\.");
Object r = queryHelper(jo, keys[0]);
for (int i = 1; i < keys.length; i++) {
r = queryHelper(jo.fromObject(r), keys[i]);
}
return r;
} catch (JSONException e) {
return "";
}
}

/* help in query array objects: Placemark[0] */
private static Object queryHelper(JSONObject jo, String query) {
int openIndex = query.indexOf('[');
int endIndex = query.indexOf(']');
if (openIndex > 0) {
String key = query.substring(0, openIndex);
int index = Integer.parseInt(query.substring(openIndex + 1, endIndex));
return jo.getJSONArray(key).get(index);
}
return jo.get(query);
}

public static void main(String[] args) throws Exception {
System.out.println(GGcoder.geocode("650 Townsend st, San Francsico, CA"));
System.out.println(GGcoder.geocode("94103"));
}
}


/* Class to hold geocode result */
public class GAddress {
public String address;
public String fullAddress;
public String zipCode;
public String city;
public String state;
public double lat;
public double lng;

/* getters and setters */

14 comments:

Andres Almiray said...

You're welcome ;-)

Jörn said...

Here's a simpler way to get just the location using Java:


public class Geocoder {
private final static String ENCODING = "UTF-8";
private final static String KEY = "xyz";

public static class Location {
public String lon, lat;

private Location (String lat, String lon) {
this.lon = lon;
this.lat = lat;
}

public String toString () { return "Lat: "+lat+", Lon: "+lon; }
}

public static Location getLocation (String address) throws IOException {
BufferedReader in = new BufferedReader (new InputStreamReader (new URL ("http://maps.google.com/maps/geo?q="+URLEncoder.encode (address, ENCODING)+"&output=csv&key="+KEY).openStream ()));
String line;
Location location = null;
int statusCode = -1;
while ((line = in.readLine ()) != null) {
// Format: 200,6,42.730070,-73.690570
statusCode = Integer.parseInt (line.substring (0, 3));
if (statusCode == 200)
location = new Location (
line.substring ("200,6,".length (), line.indexOf (',', "200,6,".length ())),
line.substring (line.indexOf (',', "200,6,".length ())+1, line.length ()));
}
if (location == null) {
switch (statusCode) {
case 400: throw new IOException ("Bad Request");
case 500: throw new IOException ("Unknown error from Google Encoder");
case 601: throw new IOException ("Missing query");
case 602: return null;
case 603: throw new IOException ("Legal problem");
case 604: throw new IOException ("No route");
case 610: throw new IOException ("Bad key");
case 620: throw new IOException ("Too many queries");
}
}
return location;
}

public static void main (String[] argv) throws Exception {
System.out.println (Geocoder.getLocation ("New York"));
}
}

Alonso said...

thx so much jorn! very useful

Thore said...

Guys you are awesome. thank you for that code - both of you! you saved me a lot of trouble for my project.

Syed said...

what are all the jars we need for the first example you provide.

Dave Cox said...

Jorn,

Thanks for your code. Exactly what I needed for my simple address to KML application.

Harsha said...

Hey i am not able to use ur code as i am getting java.net.ConnectException: Connection timed out: connect

so wat can i do for this?

sushmita said...

Will this code work in a desktop application?

NIKHIL said...

Thanks Jörn great work

Ali said...

thanks for the code guys

Your name said...

Very simple and funcional, thks man!

Evertson90 said...

Hi there, I was wondering what exactly do I have to put in the URL as I am getting a syntax error.

BufferedReader in = new BufferedReader (new InputStreamReader (new URL ("http://maps.google.com/maps/ge... (address, ENCODING)+"&output=csv&key="+KEY).openStream ()));

ET said...

This is an old post. You can try using http://code.google.com/p/geocoder-java/ it support v3.

San_shendre said...

Hi Hung,

I'm getting following exception when used above code

Exception in thread "main" java.net.SocketException: Network is unreachable: connect
    at java.net.PlainSocketImpl.socketConnect(Native Method)
    at java.net.PlainSocketImpl.doConnect(Unknown Source)
    at java.net.PlainSocketImpl.connectToAddress(Unknown Source)
    at java.net.PlainSocketImpl.connect(Unknown Source)
    at java.net.SocksSocketImpl.connect(Unknown Source)
    at java.net.Socket.connect(Unknown Source)
    at java.net.Socket.connect(Unknown Source)
    at sun.net.NetworkClient.doConnect(Unknown Source)
    at sun.net.www.http.HttpClient.openServer(Unknown Source)
    at sun.net.www.http.HttpClient.openServer(Unknown Source)
    at sun.net.www.http.HttpClient.(Unknown Source)
    at sun.net.www.http.HttpClient.New(Unknown Source)
    at sun.net.www.http.HttpClient.New(Unknown Source)
    at sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(Unknown Source)
    at sun.net.www.protocol.http.HttpURLConnection.plainConnect(Unknown Source)
    at sun.net.www.protocol.http.HttpURLConnection.connect(Unknown Source)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream(Unknown Source)
    at com.avaya.util.GGcoder.geocode(GGcoder.java:19)
    at com.avaya.util.GGcoder.geocode(GGcoder.java:52)
    at com.avaya.util.GGcoder.main(GGcoder.java:91)