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.

2 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. You mentioned that Google Maps does not have a provision for text overlays. One way I have found that that still uses Google APIs is to use the Dynamic Icon from Google Chart Tools: http://code.google.com/apis/chart/docs/gallery/dynamic_icons.html#outlined_text

    After URL-encoding the text, it’s the same as plotting an image overlay (after I get the "bullseye" text right): http://chart.apis.google.com/chart?chst=d_text_outline&chld=0000ff|14|l|ffffff|_|BULLSEYE

    ReplyDelete