The Problem
Ever used the built-in Geocoder class in Android to display a location on a map? If so, you may have found that the geocoder does not provide you with the viewport data needed to zoom the map appropriately. Given the reality of different size locations (countries, states, cities, buildings) how can we zoom the map appropriately to show the complete location on the screen, at a zoom level that’s not too high and not too low?
The Solution
In this post we will build a geocoding library called GeocoderPlus that uses the Google Maps Web Services APIs V3 to retrieve both location coordinates and viewport info. In the second part of this post, we will use the GeocoderPlus library to display locations on a map at the right zoom level. If you are simply interested in using the library without understanding the nuts and bolts, you can jump ahead to the second part.
The source code for this post is on Github at:
http://www.github.com/bricolsoftconsulting/GeocoderPlus/
GeocoderPlus
The GeocoderPlus library uses the Google Maps Web Services APIs V3 to retrieve both location coordinates and viewport info. The entire geocoding process consists of these steps:
- Create an appropriate request URL.
- Retrieve the data from the URL in JSON format.
- Parse the data from JSON into appropriate structures.
Step 1: Create the request URL
A Google Maps Web Services API V3 geocoding URL takes the form:
http://maps.googleapis.com/maps/api/geocode/output?parameters
The parameters used are:
Parameter | Description |
---|---|
address | The address you want to geocode. |
sensor | Indicates whether the device has a sensor (true / false). |
language | The language to use for the results. |
region | Specify a region to customize the results via region biasing. Region biasing gives priority to results in your region over results in other regions. |
For example, to query the location for the city of Rome and customize the results for the English language with region biasing for Great Britain, the query is:
http://maps.googleapis.com/maps/api/geocode/json?address=Rome&sensor=true&language=en_gb®ion=gb
Armed with this knowledge, we can write a function that creates the URL and appends the required parameters to it.
// Constants public static final String URL_MAPS_GEOCODE = "http://maps.googleapis.com/maps/api/geocode/json"; public static final String PARAM_SENSOR = "sensor"; public static final String PARAM_ADDRESS = "address"; public static final String PARAM_LANGUAGE = "language"; public static final String PARAM_REGION = "region"; // Members Locale mLocale; boolean mUseRegionBias = false; private String getGeocodingUrl(String locationName) { // Declare String url; Vector params; // Extract language from locale String language = mLocale.getLanguage(); // Create params params = new Vector(); params.add(new BasicNameValuePair(PARAM_SENSOR, "true")); params.add(new BasicNameValuePair(PARAM_ADDRESS, locationName)); if (language != null && language.length() > 0) { params.add(new BasicNameValuePair(PARAM_LANGUAGE, language)); } if (mUseRegionBias) { String region = mLocale.getCountry(); params.add(new BasicNameValuePair(PARAM_REGION, region)); } // Create URL String encodedParams = URLEncodedUtils.format(params, "UTF-8"); url = URL_MAPS_GEOCODE + "?" + encodedParams; // Return return url; }
Step 2: Retrieve JSON data from the URL
To retrieve the JSON data from the URL, we create a class called HttpRetriever. This class relies on two built-in classes: HttpClient and BasicResponseHandler. HttpClient issues GET requests and BasicResponseHandler processes the returned data and converts it to a string.
The section below shows the implementation of the HttpClient class. The main function we will use to retrieve data is retrieve().
package com.bricolsoftconsulting.geocoderplus.util.http; import java.io.IOException; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.DefaultHttpClient; public class HttpRetriever { // Members DefaultHttpClient httpclient = null; // Constructor public HttpRetriever() { httpclient = new DefaultHttpClient(); } // Document retrieval function public String retrieve(String url) throws ClientProtocolException, IOException { // Declare String response = null; // Connect to server and get JSON response HttpGet request = new HttpGet(url); ResponseHandler responseHandler = new BasicResponseHandler(); response = httpclient.execute(request, responseHandler); // Return return response; } }
Step 3: Parse the JSON data
At this point, the next hurdle is parsing the JSON content and storing it in Java objects. To accomplish this, we need to understand the format of the JSON data, define custom Java classes to hold the extracted data and create functions to parse the data. Let’s take a look at each point.
The JSON Format
The best way to understand the format of the JSON data is to look at the JSON string returned in response to our Rome query (see Step 1 above for URL).
{ "results" : [ { "address_components" : [ { "long_name" : "Rome", "short_name" : "Rome", "types" : [ "locality", "political" ] }, { "long_name" : "Rome", "short_name" : "RM", "types" : [ "administrative_area_level_2", "political" ] }, { "long_name" : "Lazio", "short_name" : "Lazio", "types" : [ "administrative_area_level_1", "political" ] }, { "long_name" : "Italy", "short_name" : "IT", "types" : [ "country", "political" ] } ], "formatted_address" : "Rome, Italy", "geometry" : { "bounds" : { "northeast" : { "lat" : 42.17071890, "lng" : 12.92975280 }, "southwest" : { "lat" : 41.65587379999999, "lng" : 12.23442660 } }, "location" : { "lat" : 41.89051980, "lng" : 12.49424860 }, "location_type" : "APPROXIMATE", "viewport" : { "northeast" : { "lat" : 42.17071890, "lng" : 12.92975280 }, "southwest" : { "lat" : 41.65587379999999, "lng" : 12.23442660 } } }, "types" : [ "locality", "political" ] } ], "status" : "OK" }
As you can see above, the entire result is a JSON object. Inside the JSON object there is a status field and a results array. Each item in the array is an address. Each address is broken down into 2 sections: address components and geometry. Geometry is further broken down into viewport and bounds (viewing areas) and location (address latitude and longitude).
New Java Classes
Now that we understand the JSON format, let’s define appropriate data structures for this data.
Address Class
Android’s built in Address class is based on the second version of the Google Maps Geocoding APIs. The V2 APIs have since been deprecated and replaced with V3 APIs that use different fields. Accordingly, we need to create our own Address class based on the V3 fields. We will use the following fields:
Field | Description |
---|---|
Locale | The locale specifying the language and region for the result. |
Street Number | The street number component of the address. |
Premise | The named location, usually a building or a collection of buildings with the same name. |
SubPremise | An entity below the premise level, can be a building or a collection of buildings with the same name. |
Floor | The floor component of the address, if any. |
Room | The room component of the address, if any. |
Neighborhood | The neighborhood for the location. |
Locality | The city or town. |
SubLocality | Political entity below city or town. |
AdminArea | First entity below country level. |
SubAdminArea | Second entity below country level. |
SubAdminArea2 | Third entity below country level. |
CountryName | The name of the country (e.g France, Belgium) |
CountryCode | The two letter country code (e.g. FR, BE) |
PostalCode | The postal code component of the address. |
FormattedAddress | The complete address formatted for display as a string. |
ViewPort | The viewport for the address, which indicates the area that we should use to view the location. |
Bounds | The viewport for the address, which indicates the area that we should use to view the location. For countries, the bounds include any territories and possessions, while viewport just contains the main area of the country. |
Latitude | The location’s latitude. |
Longitude | The location’s longitude. |
The implementation of this class is straight-forward, and consists of creating members fields plus getters and setters for these fields. Please see the Address.java file on Github for details.
Area and Point Classes
In the JSON data, the viewport and bounds objects are identical data structures representing a geographical area. In Java, we will represent these structures using the Area and Point classes. The Area class designates a geographical area using the northeast and southwest corners. Each corner point in turn is represented using the Point class, which consists of latitude and longitude coordinates.
Once again, the implementation of these classes is straight-forward, and consists of member fields plus getters and setters. See the Github source code for details.
JSON Parsing
GeocoderPlus uses a series of functions to parse the JSON tree. Processing starts at the top of the tree and moves down, progressively extracting data from the JSON tree and transferring it into Java objects.
- The top level function, getAddressesFromJSON(), operates on the entire JSON tree and looks for individual addresses.
- The second level function, getAddressFromJSON(), operates on each JSON address object and relies on lower levels to fill in an Address object.
- The third level functions, populateAddressComponentsFromJSON() and populateAddressGeometryFromJSON(), process the address components and geometry sections of each address. Notice how the structure of the parsing code mirrors the structure of the JSON.
- At the fourth level, the populateAddressComponentsFromJSON() function performs the field-by-field transfer of address components into the Address object. The populateAddressGeometryFromJSON() function transfers the viewport, bounds and location (latitude/ longitude) data using the getAreaFromJSON() function.
The entire parsing process is built in such a way that any missing field will not cause the entire process to halt. Many of the fields are in fact optional and depend on the type of address.
Conclusion
This post explained the inner workings of the GeocoderPlus library and how it uses the Google Maps Geocoding APIs V3 to provide geocoding results that contain viewport information. The second part of this post will explain how to use the library to display objects on a map at the just the right zoom level.
The source code for this post is on Github at:
http://www.github.com/bricolsoftconsulting/GeocoderPlus/