Search in this blog

Sunday, July 12, 2015

Java Geocoding using Google Maps Api

Java Server-Side Geocoding

I was developing a web app including a map and some markers in it, I got a decent database having many human readable addresses like "1600 Amphitheatre Parkway, Mountain View, CA", in order to put a marker in the map for a human readable address you need coordinates, Geocoding is the process of for converting human readable address into geographic coordinates (latitude, longitude).

My app is developed in Java using Google Maps, Google has a API for geocoding (https://developers.google.com/maps/documentation/geocoding/), the problem is, the API works for JavaScript only, you can find lots of examples for using it this way.

Google has a web service which can give you geocodes in json or xml, try this link: http://maps.googleapis.com/maps/api/geocode/json?address=california&sensor=false

Here we are requesting the location of "california".

Now make in it works in Java is not a hard taks, you need to write the necessary objects for representing the json structure and some a method to do a GET request and parse the json result into the Java object.

I will use Apache Http Components for doing the request and Jackson JSON Processor for parsing the request.

First, declare the requiered objects for the geocoding API (all of them should have getters and setters, DONT FORGUET TO PUT IT).

public class GoogleGeoCode {
    private String status;
    private GoogleGeoResult [] results;
    private Boolean exclude_from_slo;
    private String error_message;
}
 
public class GoogleGeoResult   {
    private GoogleGeoAdressComponent [] address_components;
    private String formatted_address;
    private GoogleGeoGeometry geometry;
    private Boolean partial_match;
    private String place_id;
    private String [] types;
 

public class GoogleGeoAdressComponent {
    private String long_name;
    private String short_name;
    private String [] types;
}
 
public class GoogleGeoGeometry {
    private GoogleGeoBounds bounds;
    private GoogleGeoLatLng location;
    private String location_type;
    private GoogleGeoBounds viewport;
}
  
public class GoogleGeoBounds   {
    private GoogleGeoLatLng northeast;
    private GoogleGeoLatLng southwest;
}
 
public class GoogleGeoLatLng {
    private String lat;
    private String lng;
 

Before doing the next I hope you had read the documentation of Google for geocoding, if not, reading it would help you to understand some things.

Google allows you to do geocoding without having an API_KEY but in most cases having it would be better, if you will use the API_KEY you should do the request using SSL (https), if you sent the KEY over HTTP, google will reject the request, the method works for BOTH (http and https):

/**
 * Given an address asks google for geocode
 *
 * If ssl is true API_KEY should be a valid developer key (given by google)
 *
 * @param address the address to find
 * @param ssl defines if ssl should be used
 * @return the GoogleGeoCode found
 * @throws Exception in case of any error
 *
 */
public GoogleGeoCode getGeoCode(String address, boolean ssl) throws Exception {
    // build url
    StringBuilder url = new StringBuilder("http");
    if ( ssl ) {
        url.append("s");
    }
  
    url.append("://maps.googleapis.com/maps/api/geocode/json?");
  
    if ( ssl ) {
        url.append("key=");
        url.append(API_KEY);
        url.append("&");
    }
    url.append("sensor=false&address=");
    url.append( URLEncoder.encode(address) );
  
    // request url like: http://maps.googleapis.com/maps/api/geocode/json?address=" + URLEncoder.encode(address) + "&sensor=false"
    // do request
    try (CloseableHttpClient httpclient = HttpClients.createDefault();) {
        HttpGet request = new HttpGet(url.toString());

        // set common headers (may useless)
        request.setHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Firefox/31.0 Iceweasel/31.6.0");
        request.setHeader("Host", "maps.googleapis.com");
        request.setHeader("Connection", "keep-alive");
        request.setHeader("Accept-Language", "en-US,en;q=0.5");
        request.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
        request.setHeader("Accept-Encoding", "gzip, deflate");

        try (CloseableHttpResponse response = httpclient.execute(request)) {
            HttpEntity entity = response.getEntity();

            // recover String response (for debug purposes)
            StringBuilder result = new StringBuilder();
            try (BufferedReader in = new BufferedReader(new InputStreamReader(entity.getContent()))) {
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    result.append(inputLine);
                    result.append("\n");
                }
            }

            // parse result
            ObjectMapper mapper = new ObjectMapper();
            GoogleGeoCode geocode = mapper.readValue(result.toString(), GoogleGeoCode.class);

            if (!"OK".equals(geocode.getStatus())) {
                if (geocode.getError_message() != null) {
                    throw new Exception(geocode.getError_message());
                }
                throw new Exception("Can not find geocode for: " + address);
            }
            return geocode;
        }
    }
}



HttpComponents and Jackson Parser made the job easier.

You may notice some request returns most than 1 result, in this case, what about for keeping the better one?

I define the better one as the result with the most similar address to the one requested (google gives you a formatted address for every result),

I used a simple approach for measuring this, the Longest Common Subsequence, the next method will help you to filter the results into the best one, I used in my app and seems to work really well.

/**
 * Given an address and google geocode find the most probable location of
 * address, the measure uses the longest common subsequence algorithm and a
 * minimum requirement for similarity
 *
 * @param address the original address
 * @param geocode the google geocode results
 * @return the most probable location (lat, lng), null if no one matches
 */
public GoogleGeoLatLng getMostProbableLocation(String address, GoogleGeoCode geocode) {
    address = address.toLowerCase();
    int expected = address.length() / 2;
    int sz = geocode.getResults().length;
    int best = expected;
    GoogleGeoLatLng latlng = null;
    for (GoogleGeoResult result : geocode.getResults()) {
        GoogleGeoLatLng cur = result.getGeometry().getLocation();
        String formattedAddress = result.getFormatted_address().toLowerCase();
        int p = lcs(address, formattedAddress);

        if (p > best) {
            latlng = cur;
            best = p;
        }
    }
    return latlng;
}




And the LCS method:

/**
 * The longest common subsequence of s and t using dynamic programming
 *
 * @param s the first string
 * @param t the second string
 * @return the length of the longest common subsequence
 */
private int lcs(String s, String t) {
    int N = s.length();
    int M = t.length();
    int[][] ans = new int[N + 1][M + 1];
    for (int k = N - 1; k >= 0; k--) {
        for (int m = M - 1; m >= 0; m--) {
            if (s.charAt(k) == t.charAt(m)) {
                ans[k][m] = 1 + ans[k + 1][m + 1];
            } else {
                ans[k][m] = Math.max(ans[k + 1][m], ans[k][m + 1]);
            }
        }
    }
    return ans[0][0];
}



I packet them into a simple class:

/**
 * Utils for Google geocoding api
 * 
 * @author Alexis Hernandez
 */
public class GoogleGeoUtils {
 public GoogleGeoCode getGeoCode(String address, boolean ssl); 
 public GoogleGeoLatLng getMostProbableLocation(String address, GoogleGeoCode geocode);
 private int lcs(String s, String t);
}


I may attach the sources later.

IMPORTANT NOTE: I used HttpClient 4.5 (the current latest version) but some previous versions have issues requesting google apis using SSL.


After I wrote the code I found a library which appears to do the work: https://code.google.com/p/geocoder-java/

If you want to, give it a try, I didn't tested.


I hope it can be useful for you, thanks for reading and see you in the next post.

No comments:

Post a Comment

Leave me a comment