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.

No comments:

Post a Comment