Monday, April 19, 2010

Developing with Online Mapping APIs - Part 6: Custom Drawing

At this point we've covered all of the basics. You have all the tools you need to display an interactive map with rich information. You also have some information that will help you to decide which vendor API is best suited for your task. Now let's do something a little more fun. Let's try doing some custom drawing on our maps. Custom drawing is the ability to draw lines and polygons (filled and unfilled) on our map, and potentially to even overlay text and images. The degree to which you can plot custom elements on the map varies across mapping APIs, so let's take a look at what kind of interesting things we can do on each.

Bing

The VEShape class provides all the tools we need for doing custom drawing on a Bing map. With a VEShape instance we can:

  • Draw text
  • Plot an image
  • Draw a polyline (a line with multiple segments)
  • Draw an unfilled polygon
  • Draw a filled polygon

We can control the color of the text, line, and fill. We can also assign a transparency value (alpha) to the fill and line color. When we use a VEShape, the shape we put on the map is anchored to the map at the lattitude and longitude of the points we assign.

Adding text to the map is as simple as creating a VEShape of type VEShapeType.Pushpin and then assigning the text to display using the SetCustomIcon() method. Be sure to wrap the text in a SPAN or DIV tag, otherwise the text will be interpretted as the URL of an image. Also, the Bing map will wrap this custom HTML in an A tag, so you may want to assign some styles to your DIV or SPAN tag to be sure the text is displayed the way you want. As mentioned, as long as the argument given to the SetCustomIcon method doesn't start with a '<', it is assumed to be the URL of an image to display. This is how we can display an image on our map.

When creating a polygon using the Bing map API, an array of vertices is passed to the VEShape constructor. When drawing the shape, the API will automatically creating a line segment between the last vertex and the first vertex. The API only supports line segments (no arcs) so it is not possible to draw smooth curves. Still, we can fake up a curves by using a number of small line segments.

function bingPlotCircle(scalar)
{
 var vertexCount = 64;
 var vertices = new Array();
 var v = 0;
 var radians = Math.PI / 180;
    var radiansPerDegree = Math.PI / 180;
 var degreesPerVertex = 360 / vertexCount;
 var radiansPerVertex = degreesPerVertex * radiansPerDegree;
    var mapCenterLatLong = bingMap6.GetCenter();
 var mapView = bingMap6.GetMapView();

 var mapHeight = mapView.TopLeftLatLong.Latitude - mapCenterLatLong.Latitude;
 var mapWidth = mapView.TopLeftLatLong.Longitude - mapCenterLatLong.Longitude;

 for ( v = 0; v < vertexCount; v++ )
 {
  radians = v * radiansPerVertex;
  vertices.push( 
   new VELatLong(  
    mapCenterLatLong.Latitude + ( scalar * mapHeight * Math.sin( radians ) ), 
    mapCenterLatLong.Longitude + ( scalar * mapWidth * Math.cos( radians ) ) ) );
 }

 var circle = new VEShape(VEShapeType.Polygon, vertices);
 circle.SetFillColor( new VEColor( 128, 0, 0, 0.2 ) );
 circle.SetLineColor( new VEColor( 128, 0, 0, 0.2 ) );
 circle.SetLineWidth( 1 );
 circle.HideIcon();

 bingMap6.AddShape( circle );
}

function bingPlotLines()
{
 var vertices = new Array();
 var bounds = bingMap6.GetMapView();
 var center = bingMap6.GetCenter();
 vertices.push( new VELatLong( bounds.TopLeftLatLong.Latitude, center.Longitude ) );
 vertices.push( new VELatLong( bounds.BottomRightLatLong.Latitude, center.Longitude ) );

 var verticalLine = new VEShape( VEShapeType.Polyline, vertices );
 verticalLine.SetLineWidth(6);
 verticalLine.HideIcon();
 bingMap6.AddShape(verticalLine);

 vertices = new Array();
 vertices.push( new VELatLong( center.Latitude, bounds.TopLeftLatLong.Longitude ) );
 vertices.push( new VELatLong( center.Latitude, bounds.BottomRightLatLong.Longitude ) );

 var horizontalLine = new VEShape( VEShapeType.Polyline, vertices );
 horizontalLine.SetLineWidth(6);
 horizontalLine.HideIcon();
 bingMap6.AddShape(horizontalLine);
}

function bingPlotText()
{
 var textShape = new VEShape( VEShapeType.Pushpin, bingMap6.GetCenter() );
 textShape.SetCustomIcon("BULLSEYE");
 bingMap6.AddShape(textShape);
}

function bingPlotImage()
{
 var imageShape = new VEShape( VEShapeType.Pushpin, bingMap6.GetCenter() );
 imageShape.SetCustomIcon("target.jpg");
 bingMap6.AddShape(imageShape);
}

Google

You know the story by now - where Bing consolidates functionality to a single class, Google separates into many classes. The same holds for custom drawing. With Google Maps we can:

  • Plot an image
  • Draw a polyline (a line with multiple segments)
  • Draw an unfilled polygon
  • Draw a filled polygon

Unlike Bing, which used the VEShape class for all of these, Google uses several:

  • GIcon
    • Plot a custom image
  • GPolyline
    • Draw a polyline
  • GPolygon
    • Draw a polygon (filled or unfilled)

Another difference between the Bing and Google implementation is that with the Google classes, most of the options for the shape are set in the constructor. Using named parameters we can set the line width, color, opacity, fill color, and fill opacity.

function googlePlotCircle(scalar)
{
 var vertexCount = 64;
 var vertices = new Array();
 var v = 0;
 var radians = Math.PI / 180;
    var radiansPerDegree = Math.PI / 180;
 var degreesPerVertex = 360 / vertexCount;
 var radiansPerVertex = degreesPerVertex * radiansPerDegree;
    var center = googleMap6.getCenter();
 var bounds = googleMap6.getBounds();

 var mapHeight = bounds.getNorthEast().lat() - center.lat();
 var mapWidth = bounds.getNorthEast().lng() - center.lng();

 for ( v = 0; v < vertexCount; v++ )
 {
  radians = v * radiansPerVertex;
  vertices.push( 
   new GLatLng(  
    center.lat() + ( scalar * mapHeight * Math.sin( radians ) ), 
    center.lng() + ( scalar * mapWidth * Math.cos( radians ) ) ) );
 }

 var circle = new GPolygon(vertices, "#800000", 1, 0.2, "#800000", 0.2);
 googleMap6.addOverlay( circle );
}

function googlePlotLines()
{
 var vertices = new Array();
 var bounds = googleMap6.getBounds();
 var center = googleMap6.getCenter();
 vertices.push( new GLatLng( bounds.getSouthWest().lat(), center.lng() ) );
 vertices.push( new GLatLng( bounds.getNorthEast().lat(), center.lng() ) );

 var verticalLine = new GPolyline( vertices, "#0000FF", 6 );
 googleMap6.addOverlay(verticalLine);

 vertices = new Array();
 vertices.push( new GLatLng( center.lat(), bounds.getSouthWest().lng() ) );
 vertices.push( new GLatLng( center.lat(), bounds.getNorthEast().lng() ) );

 var horizontalLine = new GPolyline( vertices, "#0000FF", 6 );
 googleMap6.addOverlay(horizontalLine);
}

function googlePlotImage()
{
 var imageIcon = new GIcon( G_DEFAULT_ICON, "target.jpg" );
 var imageShape = new GMarker( googleMap6.getCenter(), { icon:imageIcon } );
 googleMap6.addOverlay(imageShape);
}

One key difference here is that the Google Maps API does not supply a method to plot text onto the map. There are a few workaround options. One is to create an image containing the text, but this makes internationalization of the text difficult. Another workaround is to use a GIcon marker and display the desired text when the info window is displayed. This has the drawback of not displaying the text when the info window is hidden. A third option is to implement your own overlay class that inherits from GMarker, such as this LabeledMarker demonstrated by Michael Purvis. Finally, there are third-party libraries such as ELabel that include text overlay functionality. Even with these options, not including a text overlay in the stock API seems to be an oversight in my opinion.

Yahoo

With the Yahoo Maps API we can:

  • Draw text
  • Plot an image
  • Draw a polyline (a line with multiple segments)

In the case of the Yahoo Maps API there are a few classes that play a role here:

  • YImage
    • YImage is not an overlay by itself, but instead defines how an image will be displayed. The image is then supplied as part of the constructor for a YMarker, which plots the image on the map.
  • YMarker
    • Used both for plotting standard points of interest as well as for images.
  • YCustomOverlay
    • An all purpose overlay. Supply whatever HTML you like to this element and it will be plotted on the map at the given point.
  • YPolyline
    • Draws a polyline on the map, with variable line width, color, and opacity.

Missing from this list is the ability to draw a filled polygon. Drawing an unfilled polygon is as simple as using the YPolyline and supplying an the first vertex as the last, thus joining the polygon together. However, if you are looking to have a filled shape, you are out of luck.

function yahooPlotCircle(scalar)
{
 var vertexCount = 64;
 var vertices = new Array();
 var v = 0;
 var radians = Math.PI / 180;
    var radiansPerDegree = Math.PI / 180;
 var degreesPerVertex = 360 / vertexCount;
 var radiansPerVertex = degreesPerVertex * radiansPerDegree;
    var center = yahooMap6.getCenterLatLon();
 var bounds = yahooMap6.getBoundsLatLon();

 var mapHeight = bounds.LatMax - center.Lat;
 var mapWidth = bounds.LonMax - center.Lon;

 for ( v = 0; v < vertexCount; v++ )
 {
  radians = v * radiansPerVertex;
  vertices.push( 
   new YGeoPoint(  
    center.Lat + ( scalar * mapHeight * Math.sin( radians ) ), 
    center.Lon + ( scalar * mapWidth * Math.cos( radians ) ) ) );
 }
 vertices.push( vertices[0] );

 var circle = new YPolyline(vertices, "#FF0000", 3);

 yahooMap6.addOverlay( circle );
}

function yahooPlotLines()
{
 var vertices = new Array();
 var bounds = yahooMap6.getBoundsLatLon();
 var center = yahooMap6.getCenterLatLon();
 vertices.push( new YGeoPoint( bounds.LatMax, center.Lon ) );
 vertices.push( new YGeoPoint( bounds.LatMin, center.Lon ) );

 var verticalLine = new YPolyline( vertices, "#0000FF", 6 );
 yahooMap6.addOverlay(verticalLine);

 vertices = new Array();
 vertices.push( new YGeoPoint( center.Lat, bounds.LonMax ) );
 vertices.push( new YGeoPoint( center.Lat, bounds.LonMin ) );

 var horizontalLine = new YPolyline( vertices, "#0000FF", 6 );
 yahooMap6.addOverlay(horizontalLine);
}

function yahooPlotText()
{
 var labelNode = YUtility.createNode('div', 'yahooLabelOverlay');
 labelNode.style.color = "#0000FF";
 labelNode.style.backgroundColor = "#FFFFFF";
 labelNode.innerHTML += 'BULLSEYE';
 var textOverlay = new YCustomOverlay(yahooMap6.getCenterLatLon());
 YUtility.appendNode(textOverlay, labelNode);
 yahooMap6.addOverlay(textOverlay);
}

function yahooPlotImage()
{
 var image = new YImage("target.jpg");
 var imageOverlay = new YMarker(yahooMap6.getCenterLatLon(), image);
 yahooMap6.addOverlay(imageOverlay);
}

The Yahoo Maps implementation of overlays is a little quirky, especially when it comes to polylines. You may find that your polylines are not drawn properly when changing the zoom level, especially when using a double-click to zoom.

The code necessary to plot text may look a little strange, and that is because it is. The Yahoo Maps API supplies a couple of helper classes, and here we use the YUtility class to add our HTML node that we want to draw as our text overlay. It's a little cumbersome to add to add text in this way, but it does work. I tried using the method of supplying the HTML for the text as a direct parameter to the YCustomOverlay constructor, but this did not appear to work. Your mileage may vary.

MapQuest

The MapQuest API allows us to:

  • Draw text
  • Plot an image
  • Draw a polyline (a line with multiple segments)
  • Draw an filled / unfilled polygon
  • Draw a filled / unfilled ellipse

The MapQuest API divides the drawing jobs among several overlay classes:

  • MQA.RectangleOverlay
    • Draws a rectangle based on two points defining opposite corners of the rectangle.
  • MQA.LineOverlay
    • Draws a multi-segment line (polyline).
  • MQA.PolygonOverlay
    • Draws a polygon, filled or unfilled.
  • MQA.EllipseOverlay
    • Draws an ellipse based on two points defining the opposite corners of a bounding rectangle.
  • MQA.ImageOverlay
    • Draws an image based on two points defining the opposite corners of a bounding rectangle.

The MapQuest API is unique in that it provides special overlays that reduce the number of points needed to define a rectangle and an ellipse. With the other APIs we have had to define our ellipse by creating enough line segments that the shape has the illusion of curvature. The MapQuest API actually draws a curve. Another interesting difference is that when we plot an image using the ImageOverlay, the image will stretch and shrink to cover the same area as we change our zoom level. If this is not the intent, an image can instead be supplied to a MQA.Poi to give the same effect as seen in the other mapping API's. The MapQuest API does not have a specific overlay for text, but text labels can be created by setting the HTMLContent value of a MQA.Poi.

function mapquestPlotCircle(scalar)
{
 var vertexCount = 64;
 var vertices = new MQA.LatLngCollection();
    var center = mapquestMap6.getCenter();
 var bounds = mapquestMap6.getBounds();

 var latDiff = scalar * ( center.getLatitude() - bounds.getUpperLeft().getLatitude() );
 var latMax = center.getLatitude() + latDiff;
 var latMin = center.getLatitude() - latDiff;
 var lonDiff = scalar * ( center.getLongitude() - bounds.getUpperLeft().getLongitude() );
 var lonMax = center.getLongitude() + lonDiff;
 var lonMin = center.getLongitude() - lonDiff;

 vertices.add( new MQA.LatLng( latMax, lonMax ) );
 vertices.add( new MQA.LatLng( latMin, lonMin ) );

 var circle = new MQA.EllipseOverlay();
 circle.setValue('shapePoints', vertices);
 circle.setValue('fillColor', '#800000');
 circle.setValue('fillColorAlpha', 0.2);
 circle.setValue('color', '#800000');
 circle.setValue('colorAlpha', 0.2);
 circle.setValue('borderWidth', 1);

 mapquestMap6.addShape( circle );
}

function mapquestPlotLines()
{
 var vertices = new MQA.LatLngCollection();
 var bounds = mapquestMap6.getBounds();
 var center = mapquestMap6.getCenter();
 vertices.add( new MQA.LatLng( bounds.getUpperLeft().getLatitude(), center.getLongitude() ) );
 vertices.add( new MQA.LatLng( bounds.getLowerRight().getLatitude(), center.getLongitude() ) );

 var verticalLine = new MQA.LineOverlay();
 verticalLine.setValue('shapePoints', vertices);
 verticalLine.setValue('color', '#0000FF');
 verticalLine.setValue('borderWidth', 6);
 mapquestMap6.addShape(verticalLine);

 vertices = new MQA.LatLngCollection();
 vertices.add( new MQA.LatLng( center.getLatitude(), bounds.getUpperLeft().getLongitude() ) );
 vertices.add( new MQA.LatLng( center.getLatitude(), bounds.getLowerRight().getLongitude() ) );

 var horizontalLine = new MQA.LineOverlay();
 horizontalLine.setValue('shapePoints', vertices);
 horizontalLine.setValue('color', '#0000FF');
 horizontalLine.setValue('borderWidth', 6);
 mapquestMap6.addShape(horizontalLine);
}

function mapquestPlotText()
{
 var textShape = new MQA.Poi(mapquestMap6.getCenter());
 textShape.setValue( 'HTMLContent', 'BULLSEYE');
 mapquestMap6.addShape(textShape);
}

function mapquestPlotImage()
{
 var imageShape = new MQA.ImageOverlay();
 var vertices = new MQA.LatLngCollection();
 vertices.add( mapquestMap6.getCenter() );
 var center = mapquestMap6.llToPix( mapquestMap6.getCenter() );
 var offset = new MQA.Point( center.getX() + 34, center.getY() + 50 );
 vertices.add( mapquestMap6.pixToLL( offset ) );
 imageShape.setValue('shapePoints', vertices);
 imageShape.setValue('imageURL', 'target.jpg');
 mapquestMap6.addShape(imageShape);
}

That is it!

Well, not really. There is a whole lot more to explore in these API's, but with this post we have covered all of the essentials you need to get started. Now it is time to get in there and explore, try some things, and see what you can do. I hope you have found this series informative and helpful. As you start on your own mapping projects, be sure to reference the vendor's development site for full documentation.

Wednesday, April 7, 2010

Developing with Online Mapping APIs - Part 5: Event Handling

We've covered a lot of ground, and our maps are now able to display rich data to our users. The next step is to allow users to interact with the map and to react to the user's input in a meaningful way. In order to do that we need to be able to handle mapping events. There are three general categories of events:

  • Keyboard and Mouse Events
  • Map Events
  • Point of Interest Events

Keyboard and mouse events are just what you might think. These events indicate that the mouse has been clicked, double-clicked, or a keyboard key has been pressed. In relation to the map display we can determine at what X and Y position the mouse was clicked.

Mapping events are related to changes in what is displayed. For instance, the user may utilize a control to pan the map north. This would be associated with a particular event. The same is true for changing the zoom or even the type of map that is displayed.

Point of interest events are specific actions taken on a point of interest. If the map displays several POI's, an event may be associated with a click action on an individual POI.

Each mapping API has a slightly different method for handling events. In many cases, a single user action may result in triggering a variety of events.

Bing

Interact with the map to see a log of the events that are received.
Listen for:
KeyboardMouseMap
onkeypress onclick onchangemapstyle
onkeydown ondoubleclick onchangeview
onkeyup onmousedown onendpan
  onmouseup onendzoom
  onmouseover onresize
  onmouseout onstartpan
  onmousewheel onstartzoom
  onmousemove  

The VEMap class exposes an AttachEvent method which is used to subscribe to events. The method accepts two parameters: the name of the event to listen for and a reference to a function that will be called when the event occurs.

bingMap5.AttachEvent("ondoubleclick", bingEventHandler);

The VEMap.DetachEvent method takes the same parameters and will unsubscribe from an event.

bingMap5.DetachEvent("ondoubleclick", bingEventHandler);

The event handling function will be called with a single parameter. The parameter contains the details for the event. If the event is relative to a marker or other shape that is drawn on the map, the elementID field of the event details will contain the ID of the shape. The ID for each shape is maintained by the VEMap instance. Use the VEMap.GetShapeByID method to retrieve a reference to the shape instance.

The mapX and mapY properties indicate where on the map the event took place. These may be null for events that do not have a point on the map. To convert these values to a lattitude and longitude use the VEMap.PixelToLatLong method.

function bingEventHandler(eventDetails)
{
 var eventLog = document.getElementById("bingEventLog");
 var logEntry = "event received: " + eventDetails.eventName + "\n";
 logEntry = logEntry + "\tMap X:\t" + eventDetails.mapX + "\n";
 logEntry = logEntry + "\tMap Y:\t" + eventDetails.mapY + "\n";
 logEntry = logEntry + "\tZoom:\t" + eventDetails.zoomLevel + "\n";
 logEntry = logEntry + "\tElement ID:\t" + eventDetails.elementID + "\n";
 eventLog.value = logEntry + eventLog.value;
}

I've only hooked up a portion of the events that are available, and only a selection of the event details are shown on each log entry. Be sure to read through the MSDN documentation on Bing VEMap Events for the full set of events and event details. These events allow for you to write code that reacts to the user's actions. In this way you can load data that is within the viewing area when the map is moved, follow the mouse movement across the map, and a variety of other interesting responses.

Google

Interact with the map to see a log of the events that are received.
Listen for:
GMap2GMarker
click click
dblclick dblclick
movestart  
move  
moveend  
mouseover mouseover
mousemove  
mouseout mouseout
dragstart dragstart
drag drag
dragend dragend
zoomend  

The Google API uses static methods on the GEvent class for registering event listeners. This is a similar difference to the Bing implementation as we saw with geocoding. Where the Bing API tends to roll functionality into the VEMap class as a one-stop shop for functionality, the Google API encapsulates functionality in separate classes.

The GEvent.addListener method is used to register a listener for an event. This method takes three parameters: the object that is the source of the event, the name of the event, and a reference to the function to call when the event is fired.

GEvent.addListener(source:Object, event:String, handler:Function)

Calling this method returns a reference to a GEventListener instance. This reference is used when calling the GEvent.removeListener method to deregister a listener for an event.

GEvent.removeListener(handle:GEventListener)

The Google event handlers are much different in invocation than the Bing event handler. The Bing event handler receives single parameter which is of the same type each time. The Google event handlers receive different parameters based on the type of event that was fired. This can make developing a cetralized event handler a bit of a chore. One other noticeable difference is that while Bing gives the X and Y coordinates on the map, the Google event receives lattitude and longitude where applicable.

function googleEventHandler(param1, param2)
{
 var eventLog = document.getElementById('googleEventLog');
 var eventLogEntry = 'Event fired:\n';
 if ( this == googleMap5 ) 
 { 
  eventLogEntry = eventLogEntry + '\tSource:\tMap\n';
  if ( param1 != null )
  {
   eventLogEntry = eventLogEntry + '\tParam1:\t' + param1 + '\n';
  }
  if ( param2 != null )
  {
   eventLogEntry = eventLogEntry + '\tParam2:\t' + param2 + '\n';
  }
 }
 if ( this == googleMarker5 )
 { 
  eventLogEntry = eventLogEntry + '\tSource:\tMarker\n';
  if ( param1 != null )
  {
   eventLogEntry = eventLogEntry + '\tLattitude:\t' + param1.lat() + '\n';
   eventLogEntry = eventLogEntry + '\tLongitude:\t' + param1.lng() + '\n';
  }
 }
 eventLog.value = eventLogEntry + eventLog.value;
}

A key difference between the Google event handlers and the Bing event handlers is that the JavaScript 'this' reference is set to the source of the event at the time when the event handler is called. This means that where code using the Bing API would need to get the element ID of the VEShape for an event, the Google event handler code can simply use the 'this' reference. It is possible to go one step further by instructing the Google API to bind an event to an object other than the source of the event using the GEvent.bind method.

GEvent.bind(source:Object, event:String, object:Object, method:Function)

When an event handler is registered in this manner, 'this' will now refer to the object instance given as the third parameter to the function. The bind method returns a reference to a GEventListener just like the addListener method. Unbinding the event handler is achieved by calling the GEvent.removeListener method with a reference to this GEventListener.

Another convenient feature of the Google event system are methods for triggering events without user action, and for clearing all event handlers. The GEvent.trigger method allows for the simulating of events on mapping elements, as well as the introduction of custom events. For instance, if you would like to simulate the user clicking on a marker (and thus fire all of the handlers for that event) you could call:

GEvent.trigger(gmarker, "click");

This will always fire all of the "click" handlers on teh gmarker instance, rather than attempting to keep track of these references on your own. In addition, you can invent custom events for any element. Simply add your handler using the same GEvent.addListener method with a reference to your custom event name. When you need to fire the custom event, call GEvent.trigger to fire it.

The GEvent class also exposes GEvent.clearListeners and GEvent.clearInstanceListeners. The GEvent.clearListeners method will remove all handlers for a specific event on an element. The GEvent.clearInstanceListeners will remove all handlers for all events.

Yahoo

Interact with the map to see a log of the events that are received.
YMapYMarkerYGeoRSS
endMapDraw endMapDraw onEndGeoRSS
changeZoom changeZoom  
startPan startPan  
onPan onPan  
endPan endPan  
startAutoPan startAutoPan  
endAutoPan endAutoPan  
MouseDown MouseDown  
MouseUp MouseUp  
MouseClick MouseClick  
MouseDoubleClick MouseDoubleClick  
MouseOver MouseOver  
MouseOut MouseOut  
KeyDown KeyDown  
KeyUp KeyUp  
onEndGeoCode openExpanded  
polyLineAdded closeExpanded  
polyLineRemoved openSmartWindow  
endMapDraw closeSmartWindow  
endMapDraw    
onEndLocalSearch    
onEndTrafficSearch    

The Yahoo Map API uses the YEvent class in much the same way that the Google API exposes the GEvent class. The YEvent class has a single static method, YEvent.Capture, for registering an event handler.

YEvent.Capture(object:[YMap|YMarker], event:EventsList, callback:Function, privateObject:Object)

As with the GEvent.addListener method, the YEvent.Capture method takes a reference to either a YMap instance or a YMarker instance, the event to listen for, and a reference to the function to call when the event is fired. In addition, the Capture method also accepts an optional object reference that will be passed to the handler when it is fired. Another difference is that the Yahoo Maps API lists all known event types as part of the EventsList object. The full list of available events can be retrieved by calling the YMap.getEventsList() method. The event list can also be found in the documentation for the YEvent class.

Unlike the Bing and Google maps API's, this is the only method for dealing with events. There is no way to deregister your handler code. Even worse, the Yahoo Maps documentation is silent on what sort of parameters are passed to the event handler for each event type. After some trial and error and snooping through the example code, I've been able to sort out a bit of the story, but it would be much better if Yahoo would make this more clear in their documentation. An event will send either one or two parameters to the event handler. The first parameter contains information on the source of the event (the YMap or the YMarker).

  • YGeoPoint
    • Lat
    • Lon
  • thisObj
  • zoomObj
    • current
    • previous

The YGeoPoint is the latitude and longitude as it relates to the event. If this is a click event, it is the location of the click. The second optional parameter has two properties, Lat and Lon, indicating the position of the mouse. As this is not documented, I can't guarantee this will be the behavior for all events, so be sure to experiment with what values you get back in your event handler.

function yahooEventHandler( eventDetails, clickDetails )
{
 var eventLog = document.getElementById('yahooEventLog');
 var logEntry = 'Event Received:\n';
 if (eventDetails != null)
 {
  if (eventDetails.thisObj == yahooMap5)
  {
   logEntry = logEntry + '\Source:\tyahooMap5\n';
  }
  if (eventDetails.thisObj == yahooMarker5)
  {
   logEntry = logEntry + '\Source:\tyahooMap5\n';
  }

  if (eventDetails.YGeoPoint != null)
  {
   logEntry = logEntry + '\tLattitude:\t' + eventDetails.YGeoPoint.Lat + '\n';
   logEntry = logEntry + '\tLongitude:\t' + eventDetails.YGeoPoint.Lon + '\n';
  }
  if (eventDetails.zoomObj != null)
  {
   logEntry = logEntry + '\tZoom Before:\t' + eventDetails.zoomObj.previous + '\n';
   logEntry = logEntry + '\tZoom After:\t' + eventDetails.zoomObj.current + '\n';
  }
 }
 if (clickDetails != null)
 {
  logEntry = logEntry + '\tMouse Lattitude:\t' + clickDetails.Lat + '\n';
  logEntry = logEntry + '\tMouse Longitude:\t' + clickDetails.Lon + '\n';
 }
 eventLog.value = logEntry + eventLog.value;
}
 

As with the Google event handling, it can be very difficult to determine just what type of event triggered the call. This makes creating a centralized event handler a bit of a chore, so you'll want to create a unique handler for each event type of you want different behavior for each type of event.

MapQuest

Interact with the map to see a log of the events that are received.
MQA.TileMapMQA.Poi
click click
dblclick dblclick
mouseup mouseup
mousedown mousedown
dragstart mouseover
dragend mouseout
drag  
zoomStart  
zoomend  

Hey, we've got our friendly MapQuest map back! Like the Yahoo and Google mapping APIs, the MapQuest API uses a class with static members for hooking up event handlers: MQA.EventManager. The MQA.EventManager has three methods:

  • MQA.EventManager.addListener(source:Object, eventType:String, handler:Function, target:Unknown)
  • MQA.EventManager.removeListener(source:Object, eventType:String, handler:Function, target:Unknown)
  • MQA.EventManager.clearListeners(source:Object, eventType:String)

When the event handler is called it receives a single parameter of type MQA.Event. Unfortunately, the properties of this class are all set based on the type of event that was fired. Couple that with a lack of documentation surrounding events, and we're back in the same position we were with the Yahoo mapping API. The best source of information I have found is in the MapQuest JavaScript API Developer's Guide, section 13 Custom Events. At the end of this section is a list of the events available for map, POI, overlay, and other mapping element types. Interpretting this documentation, we find that the MQA.Event object has the potential to contain the following data (although not all properties will always be populated, so be sure to check for null):

  • MQA.Event.eventName
    • Name of the event. This will always be present.
  • MQA.Event.button
    • Button associated with the mouse event.
  • MQA.Event.domEvent
    • Name of the DOM event correlating to this event.
  • MQA.Event.poi
    • Reference to the MQA.Poi relevant to this event.
  • MQA.Event.xy
    • Map X,Y coordinates for this event (such as a mouse click).
  • MQA.Event.ll
    • Map Lattitude and Longitude for this event.
  • MQA.Event.srcObject
    • Source of the event.
  • MQA.Event.prevZoom
    • Previous zoom level.
  • MQA.Event.zoom
    • Current zoom level.
  • MQA.Event.clientX
    • X coordinate within the visible portion of the page.
  • MQA.Event.clientY
    • Y coordinate within the visible portion of the page.
  • MQA.Event.dragPercentage
    • (guessing) percentage of the map that was dragged(?)
  • MQA.Event.dragDirection
    • (guessing) Direction the map was dragged(?)
  • MQA.Event.prevMapType
    • MapType in use prior to change in map type.
  • MQA.Event.mapType
    • MapType currently in use.

Using this in code, we can look for data in our event handler.

function mapquestEventHandler(eventDetails)
{
 var eventLog = document.getElementById('mapquestEventLog');
 var logEntry = 'Event Received:\n';
 if (eventDetails != null)
 {
  if ( eventDetails.eventName != null )
   logEntry = logEntry + '\teventName:\t' + eventDetails.eventName + '\n';
  if ( eventDetails.button != null )
   logEntry = logEntry + '\tbutton:\t' + eventDetails.button + '\n';
  if ( eventDetails.domEvent != null )
   logEntry = logEntry + '\tdomEvent:\t' + eventDetails.domEvent + '\n';
  if ( eventDetails.poi != null )
   logEntry = logEntry + '\tpoi:\t' + eventDetails.poi + '\n';
  if ( eventDetails.xy != null )
   logEntry = logEntry + '\txy:\t' + eventDetails.xy + '\n';
  if ( eventDetails.ll != null )
   logEntry = logEntry + '\tll:\t' + eventDetails.ll + '\n';
  if ( eventDetails.srcObject != null )
   logEntry = logEntry + '\tsrcObject:\t' + eventDetails.srcObject + '\n';
  if ( eventDetails.prevZoom != null )
   logEntry = logEntry + '\tprevZoom:\t' + eventDetails.prevZoom + '\n';
  if ( eventDetails.zoom != null )
   logEntry = logEntry + '\tzoom:\t' + eventDetails.zoom + '\n';
  if ( eventDetails.clientX != null )
   logEntry = logEntry + '\tclientX:\t' + eventDetails.clientX + '\n';
  if ( eventDetails.clientY != null )
   logEntry = logEntry + '\tclientY:\t' + eventDetails.clientY + '\n';
  if ( eventDetails.dragPercentage != null )
   logEntry = logEntry + '\tdragPercentage:\t' + eventDetails.dragPercentage + '\n';
  if ( eventDetails.dragDirection != null )
   logEntry = logEntry + '\tdragDirection:\t' + eventDetails.dragDirection + '\n';
  if ( eventDetails.prevMapType != null )
   logEntry = logEntry + '\tprevMapType:\t' + eventDetails.prevMapType + '\n';
  if ( eventDetails.mapType != null )
   logEntry = logEntry + '\tmapType:\t' + eventDetails.mapType + '\n';
 }
 eventLog.value = logEntry + eventLog.value;
}

With the Google and Yahoo map event handlers, we couldn't tell which event had been fired. The MapQuest API will always include the event type in the MQA.Event parameter that is passed to the event handling function. If your goal is to have an event logger, this can be very helpful.

Is That It?

While each of the APIs have some similar elements regarding event handling, what became most evident is the quality fo the documentation for each of the mapping APIs. The Yahoo mapping API is simply no help at all when trying to figure out how to handle events. Unless you are only interested in presenting static map information, this could be a real drawback when attempting to create an interactive site. The same is largely true of the MapQuest documentation. The Bing and Google mapping documentation is quite good, and each provides a slightly different method for hooking up events.

With this post we've covered all of the basics for getting a map on your page, displaying some useful information, and allowing for rich interactivity. Now it is time to get creative. In the next post I'll show you how to do some custom drawing which allows you to some very interesting things when presenting information. As always, if you are eager to dive in and learn more, visit the vendor's development site for full documentation.