code, code, etc.

The musings of a disjointed mind

Reverse Geocode Lookup Using Google Maps API on Android (Revisited)

| Comments

By popular demand, I’ve thrown together a complete demonstration project showing how reverse geocoding with Google’s API’s works. The example is built with Android SDK version 1.6 (donut). I am also using Eclipse and the Android Eclipse plugin for development.

First, a little background information. Reverse geocoding means that we have a set of GPS coordinates (latitude and longitude), and we want to use that data to get an approximate address of our location. The scenario I found myself in when I decided to use reverse geocoding was that I wanted to display the name of the city that a user was in.

Luckily, Google has an easy to use reverse geocoding service that you can use for just this purpose. One caveat before we get started: reverse geocoding is very resource intensive on Google’s side, and they will block your api calls if make too many requests within a certain amount of time. So, you want to make sure your application doesn’t fire requests too frequently – for my example I only send the request when users press a button. Okay, to start us off, let’s setup the proper application permissions for our reverse geocoding application. Open your project’s AndroidManifest.xml file

Now, for this example, the application is going to use the phone’s GPS receiver to pinpoint a location, and it will also need access to the internet to contact Google’s reverse geocoding service. Let’s add these to our application’s manifest. If you’re using Eclipse, just double click on the AndroidManifest.xml file, click on the Permissions tab on the bottom, and click Add -> Uses Permission. You’ll need to do this twice (once for each permission we are adding). The names of the two permissions we need are “android.permission.ACCESS_FINE_LOCATION” and “android.permission.INTERNET”.

If you are not using Eclipse or just want to edit the AndroidManifest.xml file yourself, the entries we are adding as follows:

android_permissions
1
2
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>

Next, let’s setup the layout of the application. For the purposes of this example, we are only going to have two TextViews (latitude & longitude), and a single button to fire the reverse geocoding request.

When your project was created, a layout xml file should have been created. In my example this file is named main.xml. In your project it might be different based on the settings you used when creating the project, but in any case the file we want to edit should exist in the res/layout directory of your project. For the purposes of this example, I’m just going to paste the completed layout xml file, since our goal here is to cover reverse geocoding (not android UI).

main.xml View on Github
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent">
<TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Latitude:"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/txtLatitude"></TextView>
<TextView android:id="@+id/TextView01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Longitude:"></TextView>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/txtLongitude"></TextView>
<Button android:id="@+id/btnReverseGeocode" android:text="What City Am I In?" android:layout_width="150px" android:layout_marginTop="275px" android:layout_marginLeft="75px" android:layout_height="50px"></Button>
</LinearLayout>

Now, for the real code! In the Main.java file (might be different for you depending on the settings used when creating the project), the first thing we need to do is wire up access to the elements on the activity form we created (the TextViews and Button). First, create private class level variables to hold the handles to the views, and a few other variables we’ll need later.

Main.java View on Github
1
2
3
4
5
private LocationManager locationManager;
private Location currentLocation;
private TextView txtLatitude;
private TextView txtLongitude;
private Button btnReverseGeocode;

Next, you want to add code in the onCreate method to initialize those views.

Main.java View on Github
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Get handles to the elements on our android activity page.
this.txtLatitude = (TextView) findViewById(R.id.txtLatitude);
this.txtLongitude = (TextView) findViewById(R.id.txtLongitude);
this.btnReverseGeocode = (Button) findViewById(R.id.btnReverseGeocode);

// Subscribe to our button's click event
this.btnReverseGeocode.setOnClickListener(
    new OnClickListener() {
        public void onClick(View v)
        {
            handleReverseGeocodeClick();
        }
    }
);

Next, let’s get access to the GPS receiver on the phone and request updates on current location

Main.java View on Github
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Get an instance of the android system LocationManager
// so we can access the phone's GPS receiver
this.locationManager =
    (LocationManager) getSystemService(Context.LOCATION_SERVICE);

// Subscribe to the location manager's updates on the current location
this.locationManager.requestLocationUpdates("gps", (long)30000, (float) 10.0, new LocationListener()
    {
        public void onLocationChanged(Location arg0)
        {
            handleLocationChanged(arg0);
        }

        public void onProviderDisabled(String arg0) {
            // TODO Auto-generated method stub

        }

        public void onProviderEnabled(String arg0) {
            // TODO Auto-generated method stub

        }

        public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
            // TODO Auto-generated method stub
        }
    });
}

Let’s take a closer look at the arguments passed into this.locationManager.requestLocationUpdates. The first argument is a string that defines what method we want to get location updates from. As you can see, we’re using GPS to get the best possible location fix. There are other methods than GPS that can be used, but that is outside the scope of this example.

The second argument and third arguments are worth noting because they define how often the location manager updates the current location. You want to keep these updates to a bare minimum, since getting a GPS fix can kill the phone’s battery. So, the second argument is a long integer that defines how many milliseconds between updates (I chose 30 seconds here). The third argument is a float that defines how many meters the phone has to move before an update fires (I chose 10 meters).

Basically with these settings you will get a location update every 30 seconds, unless the phone moves more than 10 meters. Again, I cannot stress how important it is to tweak these numbers for your application. The intended usage of my application is for a user walking around on foot (not driving). I’ve seen applications crash when using them in a moving car because the location manager is updating too frequently.

Okay, if you’ve been paying attention, you’ve noticed two undefined methods our example code is calling, handleReverseGeocodeClick, and handleLocationChanged. Let’s setup those methods now, and begin with handleLocationChanged.

Main.java View on Github
1
2
3
4
5
6
7
8
private void handleLocationChanged(Location loc)
{
    // Save the latest location
    this.currentLocation = loc;
    // Update the latitude & longitude TextViews
    this.txtLatitude.setText(Double.toString(loc.getLatitude()));
    this.txtLongitude.setText(Double.toString(loc.getLongitude()));
}

… and handleReverseGeocodeClick

Main.java View on Github
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void handleReverseGeocodeClick()
{
    if (this.currentLocation != null)
    {
        // Kickoff an asynchronous task to fire the reverse geocoding
        // request off to google
        ReverseGeocodeLookupTask task = new ReverseGeocodeLookupTask();
        task.applicationContext = this;
        task.execute();
    }
    else
    {
        // If we don't know our location yet, we can't do reverse
        // geocoding - display a please wait message
        showToast("Please wait until we have a location fix from the gps");
    }
}

Now, you’re probably thinking, what’s ReverseGeocodeLookupTask? In the Geocoder class which we’ll cover later, the http request to google is a blocking call, so unless you want the frontend GUI to lock up on the user, it’s best to make those calls asynchronously. Fortunately android has a simple class you can inherit from called AsyncTask that does just this. For the purposes of this demo, I will just paste the source code from my inherited ReverseGeocodeLookupTask class here:

Main.java View on Github
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ReverseGeocodeLookupTask extends AsyncTask <Void, Void, String>
{
    private ProgressDialog dialog;
    protected Context applicationContext;

    @Override
    protected void onPreExecute()
    {
        this.dialog = ProgressDialog.show(applicationContext, "Please wait...contacting the tubes.",
                "Requesting reverse geocode lookup", true);
    }

    @Override
    protected String doInBackground(Void... params)
    {
        String localityName = "";

        if (currentLocation != null)
        {
            localityName = Geocoder.reverseGeocode(currentLocation);
        }

        return localityName;
    }

    @Override
    protected void onPostExecute(String result)
    {
        this.dialog.cancel();
        Utilities.showToast("Your Locality is: " + result, applicationContext);
    }
}

If we look at this class, it’s pretty simple. First, it displays a dialog box to inform the user what is happening. Than, it makes the appropriate calls to our Geocoder class to perform the reverse geocode lookup. Finally, when the request returns, it closes the dialog, and shows the result on the screen.

Next, let’s look at the actual Geocoder class. This class performs the actual call to the google geocoding http service.

Geocoder.java View on Github
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package example.geocoding;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

import android.location.Location;

public class Geocoder
{
  public static String reverseGeocode(Location loc)
  {
      //http://maps.google.com/maps/geo?q=40.714224,-73.961452&output=json&oe=utf8&sensor=true_or_false&key=your_api_key
      String localityName = "";
      HttpURLConnection connection = null;
      URL serverAddress = null;

      try
      {
          // build the URL using the latitude & longitude you want to lookup
          // NOTE: I chose XML return format here but you can choose something else
          serverAddress = new URL("http://maps.google.com/maps/geo?q=" + Double.toString(loc.getLatitude()) + "," + Double.toString(loc.getLongitude()) +
                      "&output=xml&oe=utf8&sensor=true&key=" + R.string.GOOGLE_MAPS_API_KEY);
          //set up out communications stuff
          connection = null;

          //Set up the initial connection
          connection = (HttpURLConnection)serverAddress.openConnection();
          connection.setRequestMethod("GET");
          connection.setDoOutput(true);
          connection.setReadTimeout(10000);

          connection.connect();

          try
          {
              InputStreamReader isr = new InputStreamReader(connection.getInputStream());
              InputSource source = new InputSource(isr);
              SAXParserFactory factory = SAXParserFactory.newInstance();
              SAXParser parser = factory.newSAXParser();
              XMLReader xr = parser.getXMLReader();
              GoogleReverseGeocodeXmlHandler handler = new GoogleReverseGeocodeXmlHandler();

              xr.setContentHandler(handler);
              xr.parse(source);

              localityName = handler.getLocalityName();
          }
          catch (Exception ex)
          {
              ex.printStackTrace();
          }

      }
      catch (Exception ex)
      {
          ex.printStackTrace();
      }

      return localityName;
  }
}

The final piece of this puzzle is parsing the xml that is returned from google’s service. For this example I am using the java SAX (simple api for xml) parser. The final class to show here is GoogleReverseGeocodeXmlHandler. In my example, I only want the name of the city the user is in, so my XmlHandler class I’m about to show only parses that piece of information. If you want to grab more complete information (I’ll also give an example file that contains the XML returned by Google), you’ll have to add more to this class

GoogleReverseGeocodeXmlHandler.java View on Github
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class GoogleReverseGeocodeXmlHandler extends DefaultHandler
{
    private boolean inLocalityName = false;
    private boolean finished = false;
    private StringBuilder builder;
    private String localityName;

    public String getLocalityName()
    {
        return this.localityName;
    }

    @Override
    public void characters(char[] ch, int start, int length)
           throws SAXException {
        super.characters(ch, start, length);
        if (this.inLocalityName && !this.finished)
        {
            if ((ch[start] != '\n') && (ch[start] != ' '))
            {
                builder.append(ch, start, length);
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String name)
            throws SAXException
    {
        super.endElement(uri, localName, name);

        if (!this.finished)
        {
            if (localName.equalsIgnoreCase("LocalityName"))
            {
                this.localityName = builder.toString();
                this.finished = true;
            }

            if (builder != null)
            {
                builder.setLength(0);
            }
        }
    }

    @Override
    public void startDocument() throws SAXException
    {
        super.startDocument();
        builder = new StringBuilder();
    }

    @Override
    public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException
    {
        super.startElement(uri, localName, name, attributes);

        if (localName.equalsIgnoreCase("LocalityName"))
        {
            this.inLocalityName = true;
        }
    }
}

and, as promised, an example of XML that is returned from Google’s reverse geocoding service

google_example.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<?xml version="1.0" encoding="UTF-8" ?>
<kml xmlns="http://earth.google.com/kml/2.0"><Response>
  <name>37.422006,-122.084095</name>
  <Status>
    <code>200</code>
    <request>geocode</request>
  </Status>
  <Placemark id="p1">
    <address>1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA</address>
    <AddressDetails Accuracy="8" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><Country><CountryNameCode>US</CountryNameCode><CountryName>USA</CountryName><AdministrativeArea><AdministrativeAreaName>CA</AdministrativeAreaName><SubAdministrativeArea><SubAdministrativeAreaName>Santa Clara</SubAdministrativeAreaName><Locality><LocalityName>Mountain View</LocalityName><Thoroughfare><ThoroughfareName>1600 Amphitheatre Pkwy</ThoroughfareName></Thoroughfare><PostalCode><PostalCodeNumber>94043</PostalCodeNumber></PostalCode></Locality></SubAdministrativeArea></AdministrativeArea></Country></AddressDetails>
    <ExtendedData>
      <LatLonBox north="37.4251466" south="37.4188514" east="-122.0811574" west="-122.0874526" />
    </ExtendedData>
    <Point><coordinates>-122.0842090,37.4219550,0</coordinates></Point>
  </Placemark>
  <Placemark id="p2">
    <address>Googleplex, Mountain View, CA 94043, USA</address>
    <AddressDetails Accuracy="9" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><AddressLine>Googleplex</AddressLine></AddressDetails>
    <ExtendedData>
      <LatLonBox north="37.4249916" south="37.4186964" east="-122.0808784" west="-122.0871736" />
    </ExtendedData>
    <Point><coordinates>-122.0840260,37.4218440,0</coordinates></Point>
  </Placemark>
  <Placemark id="p3">
    <address>Whisman Elementary, California, USA</address>
    <AddressDetails Accuracy="0" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><AddressLine>Whisman Elementary</AddressLine></AddressDetails>
    <ExtendedData>
      <LatLonBox north="37.4778380" south="37.3917180" east="-122.0591050" west="-122.1156740" />
    </ExtendedData>
    <Point><coordinates>-122.0723816,37.4284340,0</coordinates></Point>
  </Placemark>
  <Placemark id="p4">
    <address>Mountain View, CA 94043, USA</address>
    <AddressDetails Accuracy="5" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><Country><CountryNameCode>US</CountryNameCode><CountryName>USA</CountryName><AdministrativeArea><AdministrativeAreaName>CA</AdministrativeAreaName><SubAdministrativeArea><SubAdministrativeAreaName>Santa Clara</SubAdministrativeAreaName><Locality><LocalityName>Mountain View</LocalityName><PostalCode><PostalCodeNumber>94043</PostalCodeNumber></PostalCode></Locality></SubAdministrativeArea></AdministrativeArea></Country></AddressDetails>
    <ExtendedData>
      <LatLonBox north="37.4528390" south="37.3855400" east="-122.0339720" west="-122.1079070" />
    </ExtendedData>
    <Point><coordinates>-122.0723816,37.4284340,0</coordinates></Point>
  </Placemark>
  <Placemark id="p5">
    <address>Mountain View, CA, USA</address>
    <AddressDetails Accuracy="4" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><Country><CountryNameCode>US</CountryNameCode><CountryName>USA</CountryName><AdministrativeArea><AdministrativeAreaName>CA</AdministrativeAreaName><SubAdministrativeArea><SubAdministrativeAreaName>Santa Clara</SubAdministrativeAreaName><Locality><LocalityName>Mountain View</LocalityName></Locality></SubAdministrativeArea></AdministrativeArea></Country></AddressDetails>
    <ExtendedData>
      <LatLonBox north="37.4698870" south="37.3565410" east="-122.0446720" west="-122.1178620" />
    </ExtendedData>
    <Point><coordinates>-122.0838511,37.3860517,0</coordinates></Point>
  </Placemark>
  <Placemark id="p6">
    <address>Mountain View-los Altos Union, California, USA</address>
    <AddressDetails Accuracy="0" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><AddressLine>Mountain View-los Altos Union</AddressLine></AddressDetails>
    <ExtendedData>
      <LatLonBox north="37.4778380" south="37.3044130" east="-122.0318830" west="-122.1925760" />
    </ExtendedData>
    <Point><coordinates>-122.0951062,37.3831595,0</coordinates></Point>
  </Placemark>
  <Placemark id="p7">
    <address>San Jose, California, USA</address>
    <AddressDetails Accuracy="4" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><Country><CountryNameCode>US</CountryNameCode><CountryName>USA</CountryName><AdministrativeArea><AdministrativeAreaName>CA</AdministrativeAreaName><SubAdministrativeArea><SubAdministrativeAreaName>Santa Clara</SubAdministrativeAreaName><AddressLine>San Jose</AddressLine></SubAdministrativeArea></AdministrativeArea></Country></AddressDetails>
    <ExtendedData>
      <LatLonBox north="37.4846370" south="37.0323470" east="-121.5070500" west="-122.1912920" />
    </ExtendedData>
    <Point><coordinates>-121.8562610,37.2915883,0</coordinates></Point>
  </Placemark>
  <Placemark id="p8">
    <address>Santa Clara, California, USA</address>
    <AddressDetails Accuracy="3" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><Country><CountryNameCode>US</CountryNameCode><CountryName>USA</CountryName><AdministrativeArea><AdministrativeAreaName>CA</AdministrativeAreaName><SubAdministrativeArea><SubAdministrativeAreaName>Santa Clara</SubAdministrativeAreaName></SubAdministrativeArea></AdministrativeArea></Country></AddressDetails>
    <ExtendedData>
      <LatLonBox north="37.4846370" south="36.8941550" east="-121.2081780" west="-122.2024760" />
    </ExtendedData>
    <Point><coordinates>-121.7195459,37.2938907,0</coordinates></Point>
  </Placemark>
  <Placemark id="p9">
    <address>Silicon Valley, California, USA</address>
    <AddressDetails Accuracy="3" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><AddressLine>Silicon Valley</AddressLine></AddressDetails>
    <ExtendedData>
      <LatLonBox north="37.4846370" south="36.1966780" east="-120.5965620" west="-122.2024760" />
    </ExtendedData>
    <Point><coordinates>-122.0347600,37.3625170,0</coordinates></Point>
  </Placemark>
  <Placemark id="p10">
    <address>California, USA</address>
    <AddressDetails Accuracy="2" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><Country><CountryNameCode>US</CountryNameCode><CountryName>USA</CountryName><AdministrativeArea><AdministrativeAreaName>CA</AdministrativeAreaName></AdministrativeArea></Country></AddressDetails>
    <ExtendedData>
      <LatLonBox north="42.0095169" south="32.5288320" east="-114.1312110" west="-124.4820030" />
    </ExtendedData>
    <Point><coordinates>-119.4179324,36.7782610,0</coordinates></Point>
  </Placemark>
</Response></kml>

So there it is! A complete example using Google’s reverse geocoding service. Download the full source code for this example project on Github

Comments