Showing posts with label web applications. Show all posts
Showing posts with label web applications. Show all posts

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.

Friday, February 26, 2010

Developing with Online Mapping APIs - Part 1: Displaying a Map

The very first objective is to be able to display a map on a web page. There are just a few basic things we need in order to make this happen:
  • A reference to the mapping API
  • An HTML element to display the map in
  • A bit of script to initialize the map
To start, let's write a simple HTML page that will display our map.
<HTML>
<HEAD>
</HEAD>
<BODY>
<DIV id="mapDiv" style="position:relative;width:640px;height:480px"></DIV>
</BODY>
</HTML>

Nothing particularly fancy here. We've created a page with a single DIV that will be the display area for our map. There are no size restrictions on the map, but you'll want to pick something that fits well with your page design.

Next we need to add a reference to the script that will allow us to create and interact with maps. We also need a bit of custom script to create the map and set the initial display. The method for doing this is largely similar across APIs, but we will look at them individually in the following sections to highlight the differences in each.

Bing

To embed a Bing map on your page the first thing you will need to add is the reference to the Bing map JavaScript API.
<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>

The only item of interest in the script reference is the v=6.2. This indicates the version of the mapping API that your code would prefer to use. This can be useful if there is a API change resulting in behavior that breaks your interface. Next, we need to add a bit of code to initialize our map. We will want to create a global variable to hold a reference to our map object. For this example, we will initialize the map on the page's onload event. There are two steps to initializing the map. The first step is to create the map object. The second step is to load the map content centered on a location.

<HTML>
<HEAD>
<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
<SCRIPT>
var bingMap;
function bingInitializer()
{
bingMap = new VEMap('mapDiv');
bingMap.LoadMap(new VELatLong(39.768519, -86.158041), 13);
}
</SCRIPT>
</HEAD>
<BODY onload="bingInitializer()">
<DIV id="mapDiv" style="position:relative;width:640px;height:480px"></DIV>
</BODY>
</HTML>

The constructor for the VEMap takes the HTML element that will host the map as an argument. The LoadMap method requires a lattitude and longitude for centering the map, as well as a zoom level. Unfortunately there is no standard for zoom levels across all APIs. In the case of Bing, the smaller the number the farther out the camera view. The greater the number the closer in the view. When developing a page that will host a Bing map, be absolutely certain that the 'position:relative' style is set on the HTML element the map will be displayed within. If this style attribute is missing the Bing map will overflow beyond the bounds of the DIV in the Google Chrome and Mozilla Firefox browsers.

Google

To embed a Google map on your page the first thing you will need to add is the reference to the Google map JavaScript API.

<script type="text/javascript" src="http://maps.google.com/maps?file=api&v=2&key=abcdefg&sensor=false"></script>

There are a few new items here. Again, we have a version refere (v=2) but now we also see a key parameter. Your site must register with Google to obtain a key for using the API. Failing to do so will result in a pop-up nag on your page. The advantage of registering for a key is that it eliminates the pop-up nag, and if you decide to purchase the support, will allow access to additional calls and features. The sensor parameter is used for enabling location aware services on mobile devices. We will leave this off for now, but I may cover it a bit in a future post.

As with the Bing map, we need to write a bit of script to initialize our Google map. We will create a global variable to hold reference to the map. Just like the Bing script, the Google script needs to both create the map object and then load the map content centered on a location and zoom level.

<HTML>
<HEAD>
<SCRIPT type="text/javascript" src="http://maps.google.com/maps?file=api&v=2&key=abcdefg&sensor=false"></SCRIPT>
<SCRIPT>
var googleMap;
function googleInitializer()
{
googleMap = new GMap2(document.getElementById("mapDiv"));
googleMap.setCenter(new GLatLng(39.768519, -86.158041), 13);
}
</SCRIPT>
</HEAD>
<BODY onload="googleInitializer()">
<DIV id="mapDiv" style="position:relative;width:640px;height:480px"></DIV>
</BODY>
</HTML>

Of note here is that, while the constructor for the Google Map object (GMap2) takes the HTML element that will host the map as the argument, you must get a reference to the element rather than supplying only the element ID. The zoom level value for Google Maps works just like Bing maps.

Yahoo

To embed a Yahoo map on your page the first thing you will need to add is the reference to the Yahoo map JavaScript API.

<script type="text/javascript" src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=YD-eQRpTl0_JX2E95l_xAFs5UwZUlNQhhn7lj1H"></script>

Here again we see a version number (v=3.8) and a application ID.

No surprises when writing script to initialize the Yahoo map.

<HTML>
<HEAD>
<SCRIPT type="text/javascript" src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=YD-eQRpTl0_JX2E95l_xAFs5UwZUlNQhhn7lj1H"></SCRIPT>
<SCRIPT>
var yahooMap;
function yahooInitializer()
{
yahooMap = new YMap(document.getElementById('mapDiv'));
yahooMap.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
}
</SCRIPT>
</HEAD>
<BODY onload="yahooInitializer()">
<DIV id="mapDiv" style="position:relative;width:640;height:480"></DIV>
</BODY>
</HTML>

The only difference to note between the Yahoo map and the other mapping APIs is that the zoom value is reversed. Lower numbers mean closer to the ground while higher numbers mean farther away. One interesting quirk: the yahoo scripts for displaying a map use a global JavaScript variable 'i'. This is a common counting variable, and I'm guessing a bug on Yahoo's part. In any case, if you are using 'i' as a variable in the same code that is loading a map, you are highly advised to rename the variable to something more descriptive lest you find the Yahoo code has given it a new and unexpected value.

MapQuest

To embed a MapQuest map on your page the first thing you will need to add is the reference to the MapQuest map JavaScript API.

<script type="text/javascript" src="http://btilelog.access.mapquest.com/tilelog/transaction?transaction=script&key=mjtd%7Clu6t2h07n1%2C2x%3Do5-lw7l9&itk=true&v=5.3.s&ipkg=controls1,traffic&ipr=false"></script>

Again we see a key parameter (key=) and a version number parameter (v=). There are a few new players here though. The itk parameter is needed to include the Tile Map Toolkit in the scripts. Make sure this is part of the include string, and always set to true. The ipkg paramter indicates which features of the map are available to the user. Make sure this includes the controls1 value at a minimum. The ipr parameter is to indicate if you are using the premium features of the API (true) or the free version (false).

<HTML>
<HEAD>
<SCRIPT type="text/javascript" src="http://btilelog.access.mapquest.com/tilelog/transaction?transaction=script&key=mjtd%7Clu6t2h07n1%2C2x%3Do5-lw7l9&itk=true&v=5.3.s&ipkg=controls1,traffic&ipr=false"></SCRIPT>
<SCRIPT>
var mapquestMap;
function mapquestInitializer()
{
mapquestMap = new MQA.TileMap(document.getElementById('mapDiv'));
mapquestMap.setCenter(new MQA.LatLng(39.768519, -86.158041), 10);
}
</SCRIPT>
</HEAD>
<BODY onload="mapquestInitializer()">
<DIV id="mapDiv" style="position:relative;width:640px;height:480px"></DIV>
</BODY>
</HTML>

The MapQuest map zoom values are like the Bing and Google maps (bigger number is closer in) but the values do not correlate one to one like the Bing and Google maps do. A couple of notes are in order with regard to the dimensions of the DIV used for displaying the MapQuest map. First, the MapQuest API will not honor percentages when used for the map dimensions. If you try to create a map with an in-line style of 100% for height and width you will find the map actually displayed at 100 pixels by 100 pixels. Second, the MapQuest map API seems to have an issue with cascading style sheets. If the DIV that is hosting your map gets the height and width set from CSS rather than an inline style attribute the API may apply the height dimension to both height and width. This bug may be addressed in the future, but at present be sure to specify the height and width values inline with the DIV tag.

Is That It?

Getting the map on the page is just the start, and in the coming posts I will show you how to do some more interesting things with the map, such as displaying a point of interest and getting a lattitude and longitude value for an address. Until then, if you would like to read more about each API you can check out the following resources.

Friday, September 4, 2009

Rural Internet

My family and I are preparing to move out of the city and into a more rural setting. As part of that move, I've been researching what sorts of internet service will be available to us. At our current residence we have a number of different options for high speed internet service. We currently use the U-Verse service offered by AT&T. We are using the 1.5Mbps service, which adds $15 / month in cost to our U-Verse television bill. Comcast also offers high speed internet service, and I'm sure we could get DSL from any number of folks. Go a few miles outside of a dense residential area and your options quickly become limited.

There are four types of service that I have found that will be available at our new location:
  • Dial-Up
  • Satellite
  • Cellular Wireless
  • Microwave Wireless
Dial-Up
Dial-up internet service is the old standby that hasn't changed in over 10 years. The top speed is still pegged at 56kbps (ignoring the "speed boosting" tech that some vendors claim). Depending on the service provider, rates run anywhere from $10-$20 / month. However, we don't plan on having a home phone, so add to this cost the price to install a home phone (around $25 / month with AT&T) for the sole purpose of using dial-up, and it comes to $35 or more per month for that sluggish dial-up internet connection. It would be useful for the most basic internet uses: browsing basic web pages and sending e-mail. Forget about online gaming or rich web media though. Compare this to our U-Verse service, which is the equivalent of 1,500kbps for $20 less each month, and it would be a serious step backward.

Satellite
There are a few satellite internet providers, Hughes and WildBlue being the most prominent. Hughes offers 1.0Mbps down, 128kbps up service for $60 / month, while WildBlue tiers their service at 512K down / 128K up for $50, or 1.5M down / 256K up for $80. Those are fine speeds, if a little pricey. The real gotcha here is latency. It used to be that with satellite internet you only received data over the dish, and all of your uploads were on your telephone line. Now you get both your up and down data from the dish, but the latency can be anywhere up to 5 seconds. Compare that to the typical sub 0.1 second latency of other internet connections, and it is a big downer. This makes the satellite internet service unusable for things like voice chat, VPN connection for working at home, or online gaming. Using a VPN connection and online gaming are high priorities for me from my internet connection, so that eliminated satellite from contention.

Cellular Wireless
Cell phone companies offer data plans for their users who have smart phones (Blackberry, iPhone, Android, etc.). This offers a fairly speedy (348kbps or faster) way to access the internet. Most carriers offer mobile broadband service with the intention that you use it occasionally with your laptop, not as your dedicated home connection. Across providers, the standard seems to be to offer up to 5GB of downloads per month for $60 / month. That may seem like a lot, but it really isn't when it is your dedicated connection for home. You can quickly exhaust that 5GB quota and start paying exorbitant rates per additional kilobyte downloaded. For example, let's say a new product revision is released, and the download is 1GB or more. If there are alternate versions, I could exhaust all 5GB in a single afternoon. The idea of these caps is to prevent folks from hogging the network with P2P applications, swapping movies all day, and to keep usage as intended: occasional use on a mobile device. The net effect for me is that cellular is not an option as a home ISP.

Microwave Wireless
I found microwave wireless service to be the best mix of speeds, price, and availability. There are several service providers that can provide service to our location. With microwave wireless you need line-of-sight to the tower providing the signal. A small antenna is mounted on your home and communicates wirelessly with the main tower. Depending on geography and tree line, these systems can offer service in a 15-30 mile radius around a tower. Prices vary by speed, with it ranging from $35 / month for 512K down, 256 up service to $90 / month for speeds over 1Mbps. There isn't a single dominant player in this market like there is in the national cellular market. The best deals I found were offered by a local company: Hoosier Broadband.

512K/256 $34.95 Residential Service
768K/384 $44.95 Residential Service
1.5M/512 $69.95 Residential Service
512K/256 $54.95 Business Service
768K/384 $64.95 Business Service

The difference between residential service and business service is that Hoosier Broadband reserves the right to lower the priority of residential traffic in deference to their business customers, and business customers are guaranteed support within 24 hours. Residential customers are not. I'm not yet decided on whether I would choose the business or residential service. As the majority of my usage is late at night, I think the residential service should suffice, but given that I also intend to use this service when I need to work from home, the business service might be more prudent. Either way, I would be selecting the 768K service, which is half of what we get now from AT&T. A cut back, yes, but not a terrible one.

DSL, FiOS, and ISDN
Just a quick note on these services: they aren't available. In fact, you probably can't remember the last time (or ever) hearing about ISDN. When I contacted the phone company, they said they are not selling new connections, but only maintaining existing accounts. That's fine, as ISDN has all of the drawbacks of dial-up with a higher cost and only barely better speeds. FiOS would be awesome, but there's no chance of getting that in a rural area as it is too expensive for Verizon to pull new fiber down country roads for a handful of customers. Maybe someday DSL will be an option, but not now.

Summary
I think that microwave wireless is one service that we will hear a lot about in the coming years. One of the president's major policy initiatives is to increase access to broadband internet service for rural Americans. Installing a tower for wireless transmission is one of the most cost effective ways to do that, and the FCC is in active talks concerning opening up more of our wireless spectrum for data traffic (this is a major reason why we had the recent switch from analog to digital television over the air). I'm hopeful that these changes will result in more options for me as rural internet consumer, and lower prices.

Monday, February 9, 2009

Google Sync

After I gave up my BlackBerry, I switched back to using my Dell Axim PDA. I'm very happy with it, and I'm having great luck with Windows Mobile 6.1 One thing that I have struggled with is to find a way to synchronize my mobile calendar, contacts, and e-mail with my Google calendar, contacts, and e-mail. Actually, I should not include e-mail, as that was always easy to do. You can synchronize either via POP or IMAP, whichever you prefer.

So next up was calendar synchronization. My goal was to find a solution that would synchronize my work calendar with my google calendar, and both my Google and work calendars onto my PDA. The first app I tried for this was Google Calendar Sync for Mobile Devices (GCSfMD). This is an application that runs on a Windows Mobile 5.0 or later device and synchronizes the calendar data on the PDA with the Google Calendar. The advantage here is that my work calendar would sync to my device, and then GCSfMD would modify the calendar to also include my Google Calendar appointments. Unfortunately GCSfMD is a one way sync. If I change an appointment on my Google Calendar my device is updated, but if I change an appointment on my device, the Google Calendar is unaltered. So my next attempt was to use Google Calendar Sync. Although the two have very similar names, they behave in very different ways. Google Calendar Sync synchronizes your Outlook calendar directly with your Google Calendar. It is a two-way sync, so if you update either your Outlook Calendar or your Google Calendar, the updates are synchronized between both. This is a great app, and I have gotten great use out of it the last few weeks.

This left contact synchronization. There simply is no good solution for synchronizing Outlook Contacts and Google Contacts. Likewise, the only application that I found that will synchronize my mobile contacts with Google Contacts is OggSync.

Fortunately, today Google released Google Sync. Google Sync works with Windows Mobile and iPhone devices to synchronize both the Calendar and Contacts with your Google data. Best of all, it required no installation on my PDA! I simply modified my ActiveSync setup to point to the Google Mobile server, and everything synced up perfectly.

A couple of questions you might have:
  • Why not just use the device web browser to check the Google sites directly?
That would be perfectly acceptable...if my device were a phone. It isn't, and I don't want to pay the extraordinary rates carriers are asking for all you can eat data service. By synchronizing my device, the data is available offline. So even if I'm in the car I still have access to all my calendar, contact, and e-mail data.
  • How do you merge the data?
I still need Google Calendar Sync. Google Sync will sync your device data with your Google data, but it will not merge with your Outlook data. I still use Google Calendar Sync to merge my Google Calendar with my Outlook calendar. There is still no solution for merging my contact data, but I'm okay with this because I can live with only having my personal contacts on my device.
  • Can I sync my device with both Google and Outlook?
At this time, no. Windows Mobile and iPhone both restrict you to a single synchronization source. You'll need to decide how you want to approach it. For me, I've chosen to make Google my primary data provider, and I sync all of the other data with Google. You may prefer to sync with your Outlook data, in which case you will need to find alternative applications to sync up.

In an ideal world, my device would sync and store all of my various calendars, contacts, e-mail, and data from as many sources as I wanted. Until then, the combination of Google Sync and Google Calendar Sync will fill the void.