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.

Sunday, March 7, 2010

Developing with Online Mapping APIs - Part 3: Marking Points of Interest

So far we've put together a page that has a map displayed, centered and zoomed on our location, and with the controls displayed as we like. The next step is to mark points of interest on our map. This is one method of providing location based information to the user. A point of interest marks a location on the map, and can provide additional details about that location.

When working with points of interest, there are a couple of things we want to do. First, we want to determine where the Point of Interest (POI) will be marked on the map. Second, we want to know how it will be marked. In some cases there are a variety of default market types to choose from. Next, we need to consider how to handle when the map contains many POIs, and when many POIs overlap on or near the same position. In the section below we will walk through each mapping API and how these concerns are addressed.

Bing

Lattitude:
Longitude:
Information Title:
Information Description:

Adding a POI to a Bing map requires creating an instance of the VEShape class and then calling the VEMap.AddShape method.

  • VEShape(VEShapeType, VELatLng)
  • VEMap.AddShape(VEShape)
  • VEMap.DeleteShape(VEShape)
  • VEMap.DeleteAllShapes()

The VEShape constructor takes a VEShapeType and the VELatLng referencing the location for the POI. For this example, we are only concerned with the VEShapeType.Pushpin (we'll discuss Polyline and Polygon later). There are two ways to remove the POI once it has been added to the map. If a reference is maintained to the POI, it may be later removed using the VEMap.DeleteShape method. Otherwise, all POIs may be removed at once by using the VEMap.DeleteAllShapes method. Managing the list of POIs can be a bit cumbersome, but it allows for more efficient use of the API.

The VEShape class has two other methods that are worth discussing here. The VEShape.SetTitle and VEShape.SetDescription methods can be used to provide details about what the POI is marking. When the mouse is hovered over the POI the title and description will be displayed to the user.

Putting all of this together, here is the sample code used on this page for adding and removing POIs to the page.

<div class="mapDiv" id="bingMapDiv3"></div>
<div>
<div>Lattitude: <input id="bingLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="bingLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="bingInfoTitle" type="text" value="Title" /></div>
<div>Information Description: <input id="bingInfoDescription" type="text" value="Description" /></div>
<div><input type="button" value="Add POI" onclick="addBingPOI();"></div>
</div>
<div>
<select id="bingPOISelect">
  <option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeBingPOI();"/>
 <script type="text/javascript">
var bingMap3;
var bingPOIs = new Array();
function bingInitializer3()
{ 
    bingMap3 = new VEMap('bingMapDiv3');
 bingMap3.SetDashboardSize(VEDashboardSize.Normal);
    bingMap3.LoadMap(new VELatLong(39.768519, -86.158041), 13);
 bingMap3.ShowDashboard();
}
scriptInitializers.push(bingInitializer3);

function addBingPOI()
{
 var title = document.getElementById('bingInfoTitle');
 var description = document.getElementById('bingInfoDescription');
 var lat = document.getElementById('bingLat');
 var lng = document.getElementById('bingLng');
 var newPOI = new VEShape(VEShapeType.Pushpin, new VELatLong(parseFloat(lat.value), parseFloat(lng.value)));
 newPOI.SetTitle(title.value);
 newPOI.SetDescription(description.value);
 
 bingMap3.AddShape(newPOI);

 var bingPOISelect = document.getElementById('bingPOISelect');
 var newPOIOption = document.createElement('option');
    newPOIOption.text = title.value;

 var poiIndex = 0;
 for ( poiIndex = 0; poiIndex < bingPOIs.length; poiIndex++ )
 {
  if ( bingPOIs[poiIndex] == null )
  {
      newPOIOption.value = poiIndex;
   bingPOISelect.add(newPOIOption);
   bingPOIs[poiIndex] = newPOI;
   return;
  }
 }
 newPOIOption.value = bingPOIs.length;
 bingPOISelect.add(newPOIOption);
 bingPOIs.push(newPOI);
}

function removeBingPOI()
{
 var bingPOISelect = document.getElementById('bingPOISelect');
 var poiIndex = parseInt(bingPOISelect.value);

 if (typeof poiIndex == 'number')
 {
  bingMap3.DeleteShape(bingPOIs[poiIndex]);
  bingPOIs[poiIndex] = null;
  bingPOISelect.remove(bingPOISelect.selectedIndex);
 }
}

</script>

These methods are fine when dealing with a handful of points of interest, but do not scale when there are many points of interest, or a variety of points of interest of different types. You may want to be able to switch on a set of points of interest or switch them off at any time. Also, you may have many points of interest that are clustered very closely together. How can we manage scaling in this way?

One way of dealing with scaling issues is to use the VEShape.SetMinZoomLevel and VEShape.SetMaxZoomLevel methods. These methods prevent the POI from being displayed at zoom levels that are too high or too low.

Another way of dealing with scaling issues is to use a VEShapeLayer. A VEShapeLayer is a way to manage a collection of VEShape instances. You might create a layer to represent a particular type. For instance, you might have one VEShapeLayer for food POIs, one for fuel POIs, and one for hotel POIs. In this way you can switch these POIs on or off all at once. A VEShapeLayer is added and removed to the map with similar methods to what we used for VEShape:

  • VEShapeLayer()
  • VEShapeLayer.AddShape(VEShape)
  • VEShapeLayer.DeleteShape(VEShape)
  • VEShapeLayer.DeleteAllShapes()
  • VEMap.AddShapeLayer(VEShapeLayer)
  • VEMap.DeleteShapeLayer(VEShapeLayer)
  • VEMap.DeleteAllShapeLayers()

In addition, the VEShapeLayer object provides a mechanism for managing VEShape instances that are tightly clustered together. The VEShapeLayer.SetClusteringConfiguration method accepts a type of either VEClusteringType.None for no clustering or VEClusteringType.Grid which uses a simple clustering algorithm.

Google

Lattitude:
Longitude:
Information Title:

Adding a POI to a Google map requires creating an instance of the GMarker class and then calling the GMap2.addOverlay method.

  • GMarker(GLatLng)
  • GMap2.addOverlay(GOverlay)
  • GMap2.removeOverlay(GOverlay)
  • GMap2.clearOverlays()

The GMarker class derives from the GOverlay class. The constructor takes a GLatLng referencing the location for the POI. There are two ways to remove the POI once it has been added to the map. If a reference is maintained to the POI, it may be later removed using the GMap2.removeOverlay method. Otherwise, all POIs may be removed at once by using the GMap2.clearOverlays method.

We saw in the Bing map that a Title and Description can be supplied that will be displayed when the mouse hovers over the POI. Google Maps handles this a bit differently. The constructor for the GMarker can also accept a second parameter of type GMarkerOptions. The GMarkerOptions class has a title property that contains the text to be displayed as a tooltip when the mouse hovers over the POI.

Putting all of this together, here is the sample code used on this page for adding and removing POIs to the page.

<div class="mapDiv" id="googleMapDiv3"></div>
<div>
<div>Lattitude: <input id="googleLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="googleLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="googleInfoTitle" type="text" value="Title" /></div>
<div><input type="button" value="Add POI" onclick="addGooglePOI();"></div>
</div>
<div>
<select id="googlePOISelect">
  <option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeGooglePOI();"/>
<script type="text/javascript">
var googleMap3;
function googleInitializer3()
{
 googleMap3 = new GMap2(document.getElementById("googleMapDiv3"));
 googleMap3.setCenter(new GLatLng(39.768519, -86.158041), 13); 
 googleCurrentMapControlStyle = new GLargeMapControl3D();
 googleMap3.addControl(googleCurrentMapControlStyle);
}
scriptInitializers.push(googleInitializer3);

function addGooglePOI()
{
 var title = document.getElementById('googleInfoTitle');
 var lat = document.getElementById('googleLat');
 var lng = document.getElementById('googleLng');
 var newPOI = new GMarker(new GLatLng(parseFloat(lat.value), parseFloat(lng.value)), { title: title.value });
 
 googleMap3.addOverlay(newPOI);

 var googlePOISelect = document.getElementById('googlePOISelect');
 var newPOIOption = document.createElement('option');
    newPOIOption.text = title.value;

 var poiIndex = 0;
 for ( poiIndex = 0; poiIndex < googlePOIs.length; poiIndex++ )
 {
  if ( googlePOIs[poiIndex] == null )
  {
      newPOIOption.value = poiIndex;
   googlePOISelect.add(newPOIOption);
   googlePOIs[poiIndex] = newPOI;
   return;
  }
 }
 newPOIOption.value = googlePOIs.length;
 googlePOISelect.add(newPOIOption);
 googlePOIs.push(newPOI);
}

function removeGooglePOI()
{
 var googlePOISelect = document.getElementById('googlePOISelect');
 var poiIndex = parseInt(googlePOISelect.value);

 if (typeof poiIndex == 'number')
 {
  googleMap3.removeOverlay(googlePOIs[poiIndex]);
  googlePOIs[poiIndex] = null;
  googlePOISelect.remove(googlePOISelect.selectedIndex);
 }
}

</script>

As before, these methods are fine when dealing with a handful of points of interest, but do not scale when there are many points of interest, or a variety of points of interest of different types. The GMarkerManager class is available to handle hundreds of markers in an efficient manner. GMarker instances are added in groups to the GMarkerManager, and the appropriate maximum and minimum zoom levels at which those POIs should be visible is set as part of that call. The GMarkerManager has been marked as deprecated at this time with Google recommending the use of the open source MarkerManager API instead. As this series focuses on the vanilla, non-beta versions of each vendor's API, I will not be covering that library here. The Google Maps API does not have an equivalent of the VEShapeLayer class found in the Bing API.

Yahoo

Lattitude:
Longitude:
Information Title:

Adding a POI to a Yahoo map requires creating an instance of the YMarker class and then calling the YMap.addOverlay method.

  • YMarker(YGeoPoint)
  • YMap.addMarker(YGeoPoint)
  • YMap.removeMarker(Id)
  • YMap.addOverlay(YOverlay)
  • YMap.removeOverlay(YOverlay)
  • YMap.removeMarkersAll()

The YMarker class derives from the YOverlay class. The constructor takes a YGeoPoint referencing the location for the POI. The YMap also includes a the addMarker method for adding a POI using only the YGeoPoint. I recommend against using this form because you are not able to maintain a reference to the marker in this way without performing further queries on the map.

As we saw in the Bing and Google maps, the Yahoo maps API allows for setting a label on the marker. Unlike the Bing and Google maps, the label is always displayed. The label is applied by using the YMarker.addLabel method to apply the label the first time, and YMarker.reLabel to change the label.

Putting all of this together, here is the sample code used on this page for adding and removing POIs to the page.

<div class="mapDiv" id="yahooMapDiv3"></div>
<div>
<div>Lattitude: <input id="yahooLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="yahooLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="yahooInfoTitle" type="text" value="Title" /></div>
<div><input type="button" value="Add POI" onclick="addYahooPOI();"></div>
</div>
<div>
<select id="yahooPOISelect">
  <option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeYahooPOI();"/>
<script type="text/javascript">
var yahooMap3;
var yahooPOIs = new Array();
function yahooInitializer3()
{
 yahooMap3 = new YMap(document.getElementById('yahooMapDiv3'));
 yahooMap3.addPanControl();
 yahooMap3.addTypeControl();
 yahooMap3.addZoomLong();
 yahooMap3.addZoomScale();
 yahooMap3.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
}
scriptInitializers.push(yahooInitializer3);

function addYahooPOI()
{
 var title = document.getElementById('yahooInfoTitle');
 var lat = document.getElementById('yahooLat');
 var lng = document.getElementById('yahooLng');
 var newPOI = new YMarker(new YGeoPoint(parseFloat(lat.value), parseFloat(lng.value)));
 newPOI.addLabel(title.value);
 
 yahooMap3.addOverlay(newPOI);

 var yahooPOISelect = document.getElementById('yahooPOISelect');
 var newPOIOption = document.createElement('option');
    newPOIOption.text = title.value;

 var poiIndex = 0;
 for ( poiIndex = 0; poiIndex < yahooPOIs.length; poiIndex++ )
 {
  if ( yahooPOIs[poiIndex] == null )
  {
      newPOIOption.value = poiIndex;
   yahooPOISelect.add(newPOIOption);
   yahooPOIs[poiIndex] = newPOI;
   return;
  }
 }
 newPOIOption.value = yahooPOIs.length;
 yahooPOISelect.add(newPOIOption);
 yahooPOIs.push(newPOI);
}

function removeYahooPOI()
{
 var yahooPOISelect = document.getElementById('yahooPOISelect');
 var poiIndex = parseInt(yahooPOISelect.value);

 if (typeof poiIndex == 'number')
 {
  yahooMap3.removeOverlay(yahooPOIs[poiIndex]);
  yahooPOIs[poiIndex] = null;
  yahooPOISelect.remove(yahooPOISelect.selectedIndex);
 }
}

</script>

We've seen from the first two posts that the Yahoo map API tends to be the most terse. This pattern continues with POI management, as there are no methods in the core API to assist with managing large sets of POIs, or visibility of POIs at various zoom levels. The YMarker class has hide and unhide methods for managing the visibility of the POI in your own code, but this will not be managed by the map.

MapQuest

Lattitude:
Longitude:
Information Title:

Adding a POI to a Mapquest map requires creating an instance of the MQA.Poi class and then calling the MQA.TileMap.addShape method.

  • MQA.Poi(MQA.LatLng)
  • MQA.TileMap.addShape(VEShape)
  • MQA.TileMap.removeShape(VEShape)
  • MQA.TileMap.removeAllShapes()

The MQA.Poi constructor takes a MQA.LatLng referencing the location for the POI. There are two ways to remove the POI once it has been added to the map. If a reference is maintained to the POI, it may be later removed using the MQA.TileMap.removeShape method. Otherwise, all POIs may be removed at once by using the MQA.TileMap.removeAllShapes method.

As we saw in the Bing and Google maps, the MQA.Poi class may also show a label when the mouse is hovered over the POI by setting the MQA.Poi.infoTitleHTML property.

Putting all of this together, here is the sample code used on this page for adding and removing POIs to the page.

<div class="mapDiv" id="mapquestMapDiv3" style="width:640px;height:480px;"></div>
<div>
<div>Lattitude: <input id="mapquestLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="mapquestLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="mapquestInfoTitle" type="text" value="Title" /></div>
<div><input type="button" value="Add POI" onclick="addMapquestPOI();"></div>
</div>
<div>
<select id="mapquestPOISelect">
  <option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeMapquestPOI();"/>
<script type="text/javascript">
var mapquestMap3;
var mapquestPOIs = new Array();
function mapquestInitializer3()
{
 mapquestMap3 = new MQA.TileMap(document.getElementById('mapquestMapDiv3'));
 mapquestMap3.addControl(new MQA.LargeZoomControl());
 mapquestMap3.addControl(new MQA.ViewControl());
 mapquestMap3.setCenter(new MQA.LatLng(39.768519, -86.158041), 10);
}
scriptInitializers.push(mapquestInitializer3);

function addMapquestPOI()
{
 var title = document.getElementById('mapquestInfoTitle');
 var lat = document.getElementById('mapquestLat');
 var lng = document.getElementById('mapquestLng');
 var newPOI = new MQA.Poi(new MQA.LatLng(parseFloat(lat.value), parseFloat(lng.value)));
 newPOI.infoTitleHTML = title.value;
 
 mapquestMap3.addShape(newPOI);

 var mapquestPOISelect = document.getElementById('mapquestPOISelect');
 var newPOIOption = document.createElement('option');
    newPOIOption.text = title.value;

 var poiIndex = 0;
 for ( poiIndex = 0; poiIndex < mapquestPOIs.length; poiIndex++ )
 {
  if ( mapquestPOIs[poiIndex] == null )
  {
      newPOIOption.value = poiIndex;
   mapquestPOISelect.add(newPOIOption);
   mapquestPOIs[poiIndex] = newPOI;
   return;
  }
 }
 newPOIOption.value = mapquestPOIs.length;
 mapquestPOISelect.add(newPOIOption);
 mapquestPOIs.push(newPOI);
}

function removeMapquestPOI()
{
 var mapquestPOISelect = document.getElementById('mapquestPOISelect');
 var poiIndex = parseInt(mapquestPOISelect.value);

 if (typeof poiIndex == 'number')
 {
  mapquestMap3.removeShape(mapquestPOIs[poiIndex]);
  mapquestPOIs[poiIndex] = null;
  mapquestPOISelect.remove(mapquestPOISelect.selectedIndex);
 }
}

</script>

Of the examples shown so far, Bing had the most options available for dealing with scaling (visibility by zoom level, adding or removing a 'layer' of markers at once, and clustering management). Google had zoom level management, and Yahoo simply left it up to the coder. Mapquest meets or exceeds the functionality found in the Bing API. The MQA.Poi class has a minZoomLevel and maxZoomLevel property for restricting the marker to only be visible within certain zoom levels. In addition, the MQA.ShapeCollection can be used to show or hide an entire category of POIs at once.

  • MQA.ShapeCollection()
  • MQA.ShapeCollection.add()
  • MQA.ShapeCollection.remove()
  • MQA.TileMap.addShapeCollection(MQA.ShapeCollection)
  • MQA.TileMap.removeShapeCollection(MQA.ShapeCollection)
  • MQA.TileMap.replaceShapeCollection(MQA.ShapeCollection, MQA.ShapeCollection)
  • MQA.TileMap.getShapeCollections()

Corresponding to the clustering feature found in Bing maps, Mapquest maps utilize a Declutter feature by default. The MQA.Declutter object can be retrieved from the map using the MQA.TileMap.getDeclutter method. The declutter mode can be set to one of the following using the MQA.Declutter.setDeclutterMode

  • 0 - No declutter
  • 1 - Stack decluttering
  • 2 - Leader-line decluttering

Is That It?

We actually skipped quite a bit here, such as creating custom icons for our POIs and using a larger information window. Creating custom icons is a fairly involved process fraught with gotchas, but I may try to touch on this in a future advanced topics post. The information window is not a difficult topic, but it is best addressed once we know how to react to a click event on the map. Before we get to handling events, the next topic to cover is geocoding and reverse-geocoding. There is a lot more to cover, and each week we get more value from our map. As always, if you are eager to learn more just follow these links to the vendor specific API documentation.

Tuesday, March 2, 2010

Developing with Online Mapping APIs - Part 2: Adding Controls to a Map

We've cleared the first hurdle. We have a page that is displaying a map and the map is zoomed and centered on the location we want. This might be useful when the site user has no need to manipulate the map, but in many cases the user will want to pan the map around, change the zoom level, or even view a different type of map. To allow the user to do these things we need to add controls to our map.

All four mapping services support a basic set of controls that allow the user to do the following:

  • Pan the map
  • Zoom the map (in or out)
  • Set the map type (road, satellite, topographical, etc.)

Panning the map is the act of moving the current map view North, East, South, or West. Zooming controls change the area that is visible within the map, as well as the level of detail. Map types allow for viewing simple road maps, topographical maps, satellite views, oblique aerial views, and much more.

These controls are available in various sizes and styles to best suite your display, whether it is small and simple controls for a mobile device or large 3D controls for a desktop scenario. Let's take a look at the controls for our four service providers.

Bing

Map Control Style: Normal Small Tiny None
Show Scale Legend
Show Mini Map
Show Map Type:

By default, the Bing map will display a road map with controls that allow for panning and zooming, as well as selecting from the available map types (Road, Aerial, and Bird's Eye). The controls are called the Dashboard, and are displayed and hidden via the ShowDashboard and HideDashboard methods on the VEMap instance.

Bing offers five types of maps to display.

  • VEMapStyle.Road // Road map
  • VEMapStyle.Shaded // Road map with shaded contours
  • VEMapStyle.Aerial // Satellite view
  • VEMapStyle.Hybrid // Satellite view with road map overlay
  • VEMapStyle.Oblique // Low altitude flight photo view
  • VEMapStyle.Birdseye // Same as Oblique
  • VEMapStyle.BirdseyeHybrid // An oblique view with a road map overlay

The map style is set via the VEMap.SetMapStyle method. In the sample map shown above, the onchange event of the select box calls a setMapType function, which sets the map type to display.

function setMapType()
{
 var bingMapTypeSelect = document.getElementById('bingMapTypeSelect');
 if ( bingMapTypeSelect.value == 'VEMapStyle.Road' )
 {
  bingMap.SetMapStyle(VEMapStyle.Road);
 }
 if ( bingMapTypeSelect.value == 'VEMapStyle.Shaded' )
 {
  bingMap.SetMapStyle(VEMapStyle.Shaded);
 }
 if ( bingMapTypeSelect.value == 'VEMapStyle.Aerial' )
 {
  bingMap.SetMapStyle(VEMapStyle.Aerial);
 }
 if ( bingMapTypeSelect.value == 'VEMapStyle.Hybrid' )
 {
  bingMap.SetMapStyle(VEMapStyle.Hybrid);
 }
 if ( bingMapTypeSelect.value == 'VEMapStyle.Oblique' )
 {
  bingMap.SetMapStyle(VEMapStyle.Oblique);
 }
 if ( bingMapTypeSelect.value == 'VEMapStyle.Birdseye' )
 {
  bingMap.SetMapStyle(VEMapStyle.Birdseye);
 }
 if ( bingMapTypeSelect.value == 'VEMapStyle.BirdseyeHybrid' )
 {
  bingMap.SetMapStyle(VEMapStyle.BirdseyeHybrid);
 }
}

Bing offers three sizes of controls to display.

  • VEDashboardSize.Normal
  • VEDashboardSize.Small
  • VEDashboardSize.Tiny

The normal control style is the default, and includes controls for changing the map type, panning the map, and zooming. This is the best control to display for regular desktop viewing. The small control style is better suited to mobile devices with less screen real estate. Here the panning controls are missing, displaying only a small set of zoom controls and map type selectors. The tiny control style is the most terse, with only the zoom controls available. This mode is best for very small map display, such or low resolution mobile devices. The control style must be set before the map is loaded. Any attempt to change the map style after it is loaded will be ignored. The map controls can be hidden and shown after the map is loaded. The scale legend can also be hidden or displayed after the map has been loaded. The important control methods to know are:

  • VEMap.SetDashboardSize(VEDashboardSize); // Set the size of the controls (call before calling VEMap.LoadMap)
  • VEMap.ShowDashboard(); // Show the map controls
  • VEMap.HideDashboard(); // Hide the map controls
  • VEMap.ShowScalebar(); // Show the scale legend
  • VEMap.HideScalebar(); // Hide the scale legend

Bing also offers some features outside of the typical panning, scaling, and map type controls. One of these features is the inset map. The inset map displays the port of the map currently in the view and allows for easier panning. When the user pans within the inset map the current view is moved to where the inset map was panned to. The inset map may either display a road or aerial view, and can be positioned anywhere within the mapping frame.

  • VEMap.ShowMiniMap(xoffset, yoffset, VEMiniMapSize)
  • VEMiniMapSize.Large
  • VEMiniMapSize.Small

The offset and size parameters are optional. If not specified, a small inset map is displayed just below and to the right of where the map controls are displayed. The unfortunate drawback here is that it can be very difficult to properly place the inset map (for instance, if you wanted to anchor it in the bottom right corner) as the pixel dimensions of the inset map are not specified. In addition, inset map can be positioned on top of the regular controls, making them impossible to use.

If your users will be using Internet Explore, the 3D map type is available. This map type utilizes a control that the user must install. This map style allows for 3D exploration of the map, including 3D building representations. In my opinion, the 3D map offers little practical use, and the requirement for installation of an embedded control coupled with the limitation to the Internet Explorer browser only makes this a feature I would not immediately target. I have not included examples on how to use the 3D map for this reason.

Google

Map Control Style: Large 3D Large Small None
Zoom Control Style: Small 3D Small None
Map Type Control Style: Standard Menu Hierarchical None
Show scale legend
Show Overview Map
Show Navigation Label Control
Show Map Type:

By default, the Google map does not display any controls. The user can still pan the map by clicking and dragging, and can zoom via the mouse wheel, but there are no dedicated controls on the screen. The Google Maps API provides a helper function on the GMap2 object to initialize the controls to the 'default' options that appear when using Google Maps on a desktop. These default controls include a panning control, zooming controls including a zoom scale bar, a map scale legend, and map type selector.

Google offers fifteen types of maps to display.

  • G_NORMAL_MAP // Road map
  • G_SATELLITE_MAP // Satellite view
  • G_AERIAL_MAP // Low altitude flight photo view
  • G_HYBRID_MAP // Satellite view with road map overlay
  • G_AERIAL_HYBRID_MAP // Low altitude flight photo view with road map overlay
  • G_PHYSICAL_MAP // Topographical map
  • G_MAPMAKER_NORMAL_MAP // Street map created using Google Mapmaker
  • G_MAPMAKER_HYBRID_MAP // Transparent overlay of major streets created in Google Mapmaker
  • G_MOON_ELEVATION_MAP // Terrain map of the lunar surface
  • G_MOON_VISIBLE_MAP // Photographic map of the lunar surface
  • G_MARS_ELEVATION_MAP // Terrain map of the martian surface
  • G_MARS_VISIBLE_MAP // Photographic map of the martian surface
  • G_MARS_INFRARED_MAP // Infrared map of the martian surface
  • G_SKY_VISIBLE_MAP // Map of the sky, displaying the celestial sphere
  • G_SATELLITE_3D_MAP // Utilizes Google Earth plug-in to display 3D model

While the moon, Mars, and Sky views are fun, for the purpose of this introduction I am going to concentrate on the typical road and satellite views. When the aerial view is unavailable the map will either display satellite imagery, or display empty tiles. In order to display aerial imagery the enableRotation method must be called on the map as well. The Mapmaker map types are composed of tiles submitted to Google Mapmaker from around the world. This map type can be useful when viewing areas of the world with poor map coverage through the standard mapping types. The map type can be set via code using the GMap2.setMapType method.

function setGoogleMapType()
{
 var googleMapTypeSelect = document.getElementById('googleMapTypeSelect');
 if ( googleMapTypeSelect.value == 'G_NORMAL_MAP' )
 {
  googleMap2.setMapType(G_NORMAL_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_SATELLITE_MAP' )
 {
  googleMap2.setMapType(G_SATELLITE_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_AERIAL_MAP' )
 {
  googleMap2.setMapType(G_AERIAL_MAP);
  googleMap2.enableRotation();
 }
 if ( googleMapTypeSelect.value == 'G_HYBRID_MAP' )
 {
  googleMap2.setMapType(G_HYBRID_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_AERIAL_HYBRID_MAP' )
 {
  googleMap2.setMapType(G_AERIAL_HYBRID_MAP);
  googleMap2.enableRotation();
 }
 if ( googleMapTypeSelect.value == 'G_PHYSICAL_MAP' )
 {
  googleMap2.setMapType(G_PHYSICAL_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_MAPMAKER_NORMAL_MAP' )
 {
  googleMap2.setMapType(G_MAPMAKER_NORMAL_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_MAPMAKER_HYBRID_MAP' )
 {
  googleMap2.setMapType(G_MAPMAKER_HYBRID_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_MOON_ELEVATION_MAP' )
 {
  googleMap2.setMapType(G_MOON_ELEVATION_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_MOON_VISIBLE_MAP' )
 {
  googleMap2.setMapType(G_MOON_VISIBLE_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_MARS_ELEVATION_MAP' )
 {
  googleMap2.setMapType(G_MARS_ELEVATION_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_MARS_VISIBLE_MAP' )
 {
  googleMap2.setMapType(G_MARS_VISIBLE_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_MARS_INFRARED_MAP' )
 {
  googleMap2.setMapType(G_MARS_INFRARED_MAP);
 }
 if ( googleMapTypeSelect.value == 'G_SKY_VISIBLE_MAP' )
 {
  googleMap2.setMapType(G_SKY_VISIBLE_MAP);
 }
}

Controls are added and removed from the Google map using GMap2.addControl(GControl) and GMap2.removeControl(GControl) respectively. The API supports a variety of pre-defined controls. There are three primary categories of controls: Panning and Zooming Controls, Zooming Only Controls, and Map Type Controls.

  • Panning and Zooming Controls
    • GLargeMapControl3D
    • GLargeMapControl
    • GSmallMapControl
  • Zooming Only Controls
    • GSmallZoomControl3D
    • GSmallZoomControl
  • Map Type Controls
    • GMapTypeControl
    • GMenuMapTypeControl
    • GHierarchicalMapTypeControl

The combined panning and zooming controls are useful for desktop browsing. The small version of the combined control is a good fit for small sidebar maps or mobile devices. The independent zoom control is useful for small screen resolutions and mobile devices as well. With both Bing and Google maps it is possible to create custom controls that facilitate any of these actions. However, it is useful to note that the Bing mapping API default controls do not allow for independent display of the map type control from the panning and zooming controls.

The Google map API also provides an inset map and a scale legend control. The inset map is called the Overview map. In addition, there is a control type called the Navigation Label Control. This is a "breadcrumbs" type control which will display a list of location names corresponding to zoom levels.

  • GOverviewMapControl
  • GScaleControl
  • GNavLabelControl

As with the Bing map, the Google map controls may be placed anywhere on the display aread by using the optional x and y offset parameters to the addControl method. By default, the panning and zooming controls appear in the upper left, the map type controls in the upper right, the scale legend in the lower left, and the inset map in the lower right. The navigation label control is also in the upper right by default and will compete with the map type controls for visibility.

Just as Bing maps offers a plug-in based view using the Bing Maps 3D plug-in, Google also permits the use of the Google Earth Plug-In. This map type utilizes the Google Earth Plug-In which the user must install. This map style allows for 3D exploration of the map, including 3D building representations. I will not be including any examples specific to the Google Earth Plug-In for the same reason that I do not reference the Bing Maps 3D plug-in.

Yahoo

Show Panning Control
Show Map Type Control
Show Scale Legend
Zoom Control Style: Long Short None
Show Map Type:

By default, the Yahoo map does not display any controls. The user can still pan the map by clicking and dragging, and can zoom with double-click, but there are no controls on the screen to facilitate this. The only default control is the map scale legend.

Yahoo offers three types of maps to display.

  • YAHOO_MAP_REG // Road map
  • YAHOO_MAP_SAT // Satellite view
  • YAHOO_MAP_HYB // Satellite view with road map overlay

The map type can be set via code using the YMap.setMapType method.

function setYahooMapType(mapTypeName)
{
 if (mapTypeName == 'YAHOO_MAP_REG')
 {
  yahooMap2.setMapType(YAHOO_MAP_REG);
 }
 if (mapTypeName == 'YAHOO_MAP_SAT')
 {
  yahooMap2.setMapType(YAHOO_MAP_SAT);
 }
 if (mapTypeName == 'YAHOO_MAP_HYB')
 {
  yahooMap2.setMapType(YAHOO_MAP_HYB);
 }
}

Controls are added and removed from the Yahoo map using helper functions on the YMap instance.

  • Panning Controls
    • YMap.addPanControl()
    • YMap.removePanControl()
  • Zooming Controls
    • YMap.addZoomLong()
    • YMap.addZoomShort()
    • YMap.removeZoomControl()
  • Map Type Controls
    • YMap.addTypeControl()

Compared to the Bing and Google mapping API's, the methods for exposing controls on a Yahoo map are fairly terse. The position of the controls is not settable. Only the Zoom control has a style option, and this is to choose between a slider type zoom control and simple zoom in / zoom out controls. When switching between these styles you must first remove the existing style by calling the removeZoomControl method. The map type control can be added to the map, but is not removable.

Where both Bing and Google offer different sizes of controls, the Yahoo maps API has just one size. Fortunately the size of the controls is adequate to fit both a desktop and a mobile display.

The Yahoo map API also provides a scale legend control.

  • YMap.addZoomScale()
  • YMap.removeZoomScale()

The Yahoo map API does not include an inset map or 'breadcrump' navigation label feature. It also does not include any type of plug-in type display integration. These may sound like drawbacks, but they could also be considered a simplification of the API and display.

MapQuest

Show Panning Control
Show Map Type Control
Show Traffic Control
Zoom Control Style Large Small None
Show Map Type:

By default, the MapQuest map does not display any controls. The user can still pan the map by clicking and dragging, but there are no controls on the screen to facilitate this.

MapQuest offers three types of maps to display.

  • map // Road map
  • sat // Satellite view
  • hyb // Satellite view with road map overlay

The map type can be set via code using the MQA.TileMap.setMapType method. Keep in mind that the map types listed above are strings, not constants.

Show Map Type: <select id="mapquestMapTypeSelect" onchange="setMapquestMapType(this.value);">
  <option value="map">Road</option>
  <option value="sat">Satellite</option>
  <option value="hyb">Hybrid</option>
</select>
<script>
function setMapquestMapType(mapType)
{
 mapquestMap2.setMapType(mapType);
}
</script>

Controls are added and removed from the MapQuest map using the MQA.TileMap.addControl and MQA.TileMap.removeControl methods. The addControl method takes a reference to the control to add, as well as an optional MQA.MapCornerPlacement instance for custom placement on the map. One thing to remember is that the reference to the MapQuest JavaScript API in our header had some extra parameters that we did not see with Bing, Google, or Yahoo. One of those parameters was the ipkg parameter, which indicates which optional JavaScript packages are available. If you remember, we included the 'controls1' and 'traffic' packages in this parameter. The 'controls1' package is required for displaying any controls on the map, so if you find that you are getting strange results when attempting to add controls, make sure this package is included.

  • Panning Controls
    • MQA.PanControl
  • Zooming Controls
    • MQA.ZoomControl
    • MQA.LargeZoomControl
  • Map Type Controls
    • MQA.ViewControl

The MapQuest controls are like the Yahoo controls in that there are only a few pre-defined control types, and they only come in one size. However, unlike the Yahoo controls, the MapQuest controls may be placed anywhere on the map. You may notice in playing with the map above that the LargeZoomControl includes a panning control. When using the LargeZoomControl it is not necessary to also add a PanControl.

The MapQuest map API will always display a scale legend. In addition, the MapQuest API also has an available Traffic control. Be sure that the JavaScript API reference includes 'traffic' in the list of included optional packages and this control will be available for display.

  • MQA.TrafficControl

The traffic control allows the user to overlay a traffic congestion heat map onto the displayed map type.

The MapQuest map API does not include an inset map or 'breadcrumb' navigation label feature. It also does not include any type of plug-in type display integration. These may sound like drawbacks, but they could also be considered a simplification of the API and display.

Is That It?

In the first post of the series we conquered simply putting the map on the page. Now we've added some controls that allow our site user to move around the map, change the map type, change the zoom level, and other types of navigation. In the next post we'll take a look at putting points of interest on the map. Until then, if you would like to read more about each API you can check out the following resources.