Monday, March 29, 2010

Developing with Online Mapping APIs - Part 4: Geocoding and Reverse-Geocoding

Centering the map and placing points of interest is fairly painless when you know the lattitude and longitude of your POIs, but most people don't think about their location in terms of lat and long. Instead, most people think in terms of addresses. Geocoding is the process of converting an address to lattitude and longitude. Geocoding is a complex process, and for that reason each mapping vendor places limits on the number of geocoding requests that are permitted each day. This number is in the multiple thousands typically, so it will not impact low traffic sites, but for larger sites with many thousands or millions of visitors it can be a concern. There are two methods for coping with this limitation: minimize the necessity of geocoding by storing as much data as possible with a lattitude and longitude reference, or make arrangements with the vendor for additional geocoding requests.

Just as your site visitors are not accustomed to entering locations as lattitude and longitude, they are likewise not accustomed to reading locations as lattitude and longitude. When reading points of interest from the map, the user will prefer to read addresses instead. The process of converting from a lattitude and longitude to a address is called reverse-geocoding. When using geocoding, the address is known, so conversion to a lattitude and longitude is fairly accurate. When reverse-geocoding, however, the requested point may be very far from any known address. For this reason, reverse-geocoding can give some very strange results. The best results with reverse-geocoding are received when the geospatial coordinates to convert are near a road.

When using geocoding or reverse geocoding on your site, be sure to keep your user's privacy in mind. If you will be making the user's location public to others, it is a good idea to allow the user to specify the accuracy of the location. If your app will be taking in a lattitude and longitude from the user's location, consider making only the city, zip code, or state information readable by others rather than a full street and number address.

Bing

Click the map anywhere to reverse-geocode that point.
Address To Geocode:

Bing uses an overloaded VEMap.Find method on the map object for performing geocoding. The Find method can be used to search for a named business or category of business, a street address, a place name, an intersection, or virtually anything else you might think to write into the search bar of the Bing Maps portal. The Find method is tightly coupled with the map display, and as part of the execution of the method can be instructed to add a pin to the map automatically for all matching locations, add the POIs to an existing map layer, and to page results such that only a set number of matches are returned at a time. This flexibility can save you a lot of the overhead of re-centering the map on the proper location, adding the POI marker and other map management. However, this flexibility also makes the Find method a little confusing to use at first. For the purpose of this exercise, we are only looking to turn an address into a lattitude and longitude. For this most basic use of the Find method, the following code will suffice.

function bingGeocodeLocation(address)
{
 bingMap4.Find( null, address, null, null, null, null, null, null, null, null, bingGeocodeLocationCallback );
}

function bingGeocodeLocationCallback( shapeLayer, findResult, places, moreMatchesAvailable, errorInfo )
{
 if ( places != null )
 {
  bingMap4.DeleteAllShapes();
  var placeIndex = 0;
  for ( placeIndex = 0; placeIndex < places.length; placeIndex++ )
  {
   var nextShape = new VEShape( VEShapeType.Pushpin, places[placeIndex].LatLong );
   bingMap4.AddShape(nextShape);
  }
 }
}

We only needed to supply two parameters for our geocoding call: the address to be geocoded and a callback method to handle the results to the request. The Find method is executed asynchronously. When the call is completed the callback method passed as the last parameter will be called. If the address was found, the places parameter will contain an array of places that match the address. In simple geocoding calls the array will likely have just one element, but code should be prepared to handle multiple matches. The callback to the Find method is non-reentrant, meaning that if another call is made to the Find method before the first call completes, the results to the first Find call will be lost.

Reverse-geocoding is done using the VEMap.FindLocations method. Like the Find method this call is performed asynchronously with the last parameter being the callback method to execute when results are found. Unlike the Find method, the FindLocations method has the single purpose of reverse-geocoding a lattitude and longitude. Also, the FindLocations method is only supported in the United States.

var bingMarker4;

function bingReverseGeocodePoint(latLongToReverseGeocode)
{
 bingMap4.DeleteAllShapes();
 bingMarker4 = new VEShape(VEShapeType.Pushpin, latLongToReverseGeocode);
 bingMap4.AddShape(bingMarker4);
 bingMap4.FindLocations( clickLatLng, bingReverseGeocodePointCallback );
}

function bingReverseGeocodePointCallback( possibleAddresses )
{
 if ( possibleAddresses != null )
 {
  var addressIndex = 0;
  var addressList = "";
  for ( addressIndex = 0; addressIndex < possibleAddresses.length; addressIndex++ )
  {
   addressList = addressList + "
" + possibleAddresses[addressIndex].Name; } if (bingMarker4 != null) { bingMarker4.SetDescription(addressList); } bingMap4.ShowInfoBox(bingMarker4); } }

If any potential address matches were found for the lattitude and longitude, the parameter to the callback method will be non-null. The parameter is an array of type VEPlace. The Name property of each VEPlace gives a candidate address that the lattitude and longitude might resolve to.

Google

Click the map anywhere to reverse-geocode that point.
Address To Geocode:

Unlike Bing, Google provides Geocoding as an independent web service. The GClientGeocoder object exposes a getLatLng method that takes a string parameter containing the address to geocode, and a reference to a callback function. The callback function has one parameter, the GLatLng of the geocoded address. If the address could not be geocoded this value will be null.

The GClientGeocoder object has no information about any maps on your page. This leaves you to write the code that will act on the geocoded address. This means a bit more code for you if you want to center the map and mark the location, but it also means the geocoding method is clean and fast. In addition, the GClientGeocoder can utilize a cache that keeps a client side mapping of locations to addresses. This can greatly speed up execution when the same address could be geocoded again and again from the site.

function googleGeocodeLocation(addressToGeocode)
{
    googleGeocoder.getLatLng(addressToGeocode, googleGeocodeLocationCallback);
}

function googleGeocodeLocationCallback( place )
{
 if ( place != null )
 {
  googleMap4.clearOverlays();
  googleMap4.setCenter(place);
  googleMap4.addOverlay(new GMarker(place));
 }
}

In addition to the getLatLng method, the getLocations method can be used to lookup a lattitude and longitude for an address. The difference between getLatLng and getLocations is that the getLocations method will call the callback function with a JSON object with richer information from the geocoding execution. The JSON data is defined as follows:

  • name
  • Status
    • code
    • request
  • Placemark
    • address
    • AddressDetails
      • Country
        • CountryNameCode
        • AdministrativeArea
          • AdministrativeAreaName
          • SubAdministrativeArea
            • SubAdministrativeAreaName
            • Locality
              • LocalityName
              • Thoroughfare
                • ThoroughfareName
              • PostalCode
                • PostalCodeNumber
      • Accuracy
    • Point
      • coordinates

That is a whole lot of data. Fortunately, for the purpose of geocoding, we are only interested in the coordinates. Notice that using this method for geocoding allows for multiple results to be returned. This is rare, but possible.

The GClientGeocoder class also performs reverse-geocoding using the same getLocations method. Rather than passing a string to this method, pass a GLatLng. The same JSON content will be returned, but in this case our interest will be in the AddressDetails data. Each nesting level gives us a more specific form of the address. The geocoder / reverse-geocoder does not guarantee accuracy, nor that all of this data be populated.

function googleReverseGeocodePoint(latlng)
{
 googleMap4.clearOverlays();
 googleMarker4 = new GMarker(latlng);
 googleMap4.addOverlay(googleMarker4);
 googleGeocoder.getLocations(latlng, googleReverseGeocodePointCallback);
}

function googleReverseGeocodePointCallback( response )
{
 if ( response != null && response.Status.code == 200 )
 {
  var addressIndex = 0;
  var addressList = "<div class='googleInfoWindow'><ul>";
  for ( addressIndex = 0; addressIndex < response.Placemark.length; addressIndex++ )
  {
   addressList = addressList + "<li>" + response.Placemark[addressIndex].address;
  }
  addressList = addressList + "</ul>";

  if (googleMarker4 != null)
  {
   googleMarker4.openInfoWindowHtml(addressList);
  }
 }
}

A little side discussion here. You might look at that code and think a couple of things. One, "Hey, he used an Info Window, and we haven't seen that yet for the Google API." Well...so I cheated. Anyway, what you see there for the Info Window is about all you need to know. You can open one using the openInfoWindowHtml method on a GMarker, and the parameter can be just about any HTML you like. The second thing you might be wondering is why I set the class to 'googleInfoWindow'. When I first coded this up and ran it on the page, the Info Window was blank. Turns out, the the body color for text was being used for the text within the Info Window. White on white wasn't showing up so well, so I'm using the 'googleInfoWindow' class to be sure that my text is printed in black.

Yahoo

Address To Geocode:

Like the Bing API, the Yahoo! Maps API exposes geocoding functionality via a method on the map class: YMap.geoCodeAddress. This method takes just one parameter, which is the address to geocode.

function yahooGeocodeLocation(addressToGeocode)
{
 yahooMap4.geoCodeAddress(addressToGeocode);
}

The geocoded result is returned by the map instance firing the onEndGeoCode event (leading me to believe that I should have covered events before getting into geocoding, oh well). In order to get the result of the geocoding request, the code must register a handler for the onEndGeoCode event. This is done by using the YEvent.Capture method, which takes three parameters: the event source, the event to listen for, and the function to call when the event fires.

function yahooInitializer4()
{
 yahooMap4 = new YMap(document.getElementById('yahooMapDiv4'));
 yahooMap4.addPanControl();
 yahooMap4.addTypeControl();
 yahooMap4.addZoomLong();
 yahooMap4.addZoomScale();
 yahooMap4.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
 YEvent.Capture(yahooMap4, EventsList.onEndGeoCode, yahooOnEndGeoCodeListener);
}

The callback method receives a single parameter containing the geocoding result, if the execution succeeded. Unfortunately the Yahoo! Maps AJAX Web Services documentation does not provide a definition for the parameter that is returned. From what I can tell by inspecting the value, the parameter is defined as shown below. Note, there is no guarantee that this is correct, or that Yahoo! will continue to use this definition, so be sure to verify the response when implementing your own code.

  • eventObjectGeoCode
    • Address
    • GeoPoint
      • Lat
      • Lon
    • ThisMap
    • success

The Address is a string containing the text address that was sent to the geocoder. The GeoPoint field contains the lattitude and longitude the address resolved to. The ThisMap field contains a reference back to the map that raised the event, and the success field indicates if the geocoding execution succeeded (1 == success). The method for getting geocoding results with Yahoo! Maps is unique in that multiple listeners can be attached to the event for completion of geocoding.

function yahooOnEndGeoCodeListener(result)
{
 if ( result != null )
 {
  yahooMap4.removeMarkersAll();
  var geocodedPoint = new YGeoPoint(result.GeoPoint.Lat, result.GeoPoint.Lon);
  yahooMap4.panToLatLon(geocodedPoint);
  yahooMap4.addMarker(geocodedPoint);
 }
}

The Yahoo! Maps API does not provide a mechanism for reverse geocoding.

MapQuest

Hmmmm, no map here. That can't be good. MapQuest offers a Geocoding and Reverse-Geocoding web service separately from the JavaScript mapping API. It is a RESTful web service, and works quite well. Unfortunately, in order to use the RESTful geocoding and reverse-geocoding web services provided by MapQuest you must create a proxy on your server to access them. This is to avoid cross domain scripting (your JavaScript code may not make calls to servers it was not loaded from). Previous versions of the MapQuest API did include a MQGeocode class that allowed for geocoding as part of the MapQuest JavaScript API, but that class is not part of the current API. All hope is not lost, however. In the beta for version 6.0 of the MapQuest API the geocoding function makes a triumphant return in the form of MQA.Geocoder.

Creating a JavaScript proxy to be hosted on your server for the purpose of accessing the RESTful geocoding services provided by MapQuest is beyond the scope of this series.

Is That It?

This post highlighted some pretty drastic differences between the APIs provided by each vendor. Bing provides a highly flexible method for geocoding that is tightly coupled to the map. Google provides a clean and fast geocoding mechanism that is independent of the map. Yahoo provides base geocoding services but no reverse-geocoding. Finally, MapQuest requires the implementation of a JavaScript proxy in order to even access their geocoding services. As part of working with geocoding services we've also touched a bit on event handling in each API, as well as the Info Window provided by the Google API. Next, we'll take a more complete look at how to handle events in for each mapping system. As always, if you are eager to learn more just follow these links to the vendor specific API documentation.

No comments:

Post a Comment