June 2011
Volume 26 Number 06
Test Run - Curved Lines for Bing Maps AJAX
By James McCaffrey | June 2011
In this month’s column I present a JavaScript function that can be used to add curved lines to a Bing Maps AJAX map control and discuss the principles used to test the function.
The best way for you to see where I’m headed is to check out the image in Figure 1. It shows a Bing Maps AJAX map control of the Denver area in the west-central United States. The custom JavaScript function that’s the topic of this article is being used to draw the blue curved line from the city of Boulder—the location of the University of Colorado (which one of my daughters attends)—to the Denver International Airport (where I’ve been in and out of a lot!). The custom function is named AddBezierCurve. In addition to being a highly useful function if you ever work with Bing Maps, the AddBezierCurve routine provides a great showcase for demonstrating API testing principles and contains some interesting math that you may find useful in other programming scenarios.
Figure 1 The Custom Function AddBezierCurve Adds a Curved Line to Bing Maps
In the sections that follow, I’ll first give you a brief explanation of creating a Bezier curve, which is the underlying math technique used by AddBezierCurve. Next, I’ll walk you through the function line by line so that you’ll be able to modify the source code to suit your own needs, if necessary. I’ll finish up by describing the general principles and specific techniques you can use to test AddBezierCurve and similar JavaScript functions. This article assumes you have a basic familiarity with JavaScript but doesn’t assume you’ve programmed with the Bing Maps AJAX control library before. I think you’ll find the topics presented here interesting and useful additions to your developer and tester skill sets.
Bezier Curves
Bezier curves can be used to draw a curved line between two points. Although Bezier curves have many variations, the simplest form is called a quadratic Bezier curve and requires two endpoints, usually called P0 and P2, and an intermediate point, P1, which determines the shape of the curve. Take a look at the example in the XY graph in Figure 2.
Figure 2 Constructing Bezier Curves
Points P0, P1 and P2 are the open red circles on the graph. The two endpoints are P0 = (1,2) and P2 = (13,8). The intermediate point is P1 = (7,10). The Bezier function accepts a parameter, usually called t, which ranges from 0.0 to 1.0. Each value of t produces a point between P0 and P2 that lies on a curve.
In Figure 2 I used five values for t: 0.0, 0.25, 0.50, 0.75 and 1.00. This produced the five points shown as smaller dots on the graph. I’ll present the equations used when I go over the code for the AddBezierCurve function. You can see that the first Bezier point for t = 0.0 is (1,2) = P0, and that the last Bezier point for t = 1.0 is (13,8) = P2. Values of t = 0.0 and t = 1.0 will generate points P0 and P2 in general. If we connect the Bezier points with line segments, we’ll approximate a nice curve between P0 and P2. More values of t generate more points, which create a smoother curve approximation.
Given endpoints P0 and P2, the value of P1 determines the shape of the resulting Bezier curve. In the example in Figure 2, I arbitrarily picked a point that’s halfway between P0 and P2 on the horizontal axis and slightly above the highest point (P2) on the vertical axis. If I had skewed P1 to the left a bit, the curve would shift to the left and become more symmetric. If I had increased the height of P1, the curve would have been taller and more peaked.
Calling the AddBezierCurve Function
In Figure 1 you can see that my map is on a Web page named CurveDemo.html (available in the code download). The overall structure of CurveDemo.html is presented in Figure 3.
Figure 3 The Demo Web Page Structure
<html>
<!-- CurveDemo.html -->
<head>
<title>Bing Maps AJAX Bezier Curve</title>
<script type="text/javascript"
src="https://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.3">
</script>
<script type="text/javascript">
var map = null; // global VEMap object
function AddBezierCurve(start, finish, arcHeight, skew, color, width,
upDown, numSegments)
{
// Code here
}
function MakeMap()
{
map = new VEMap('myMap');
map.LoadMap(new VELatLong(39.8600, -105.0000), 10, VEMapStyle.Road);
var start = new VELatLong(40.0200, -105.2700); // Boulder
var finish = new VELatLong(39.9000, -104.7000); // airport
var arcHeight = 0.20;
var skew = -0.004;
var color = new VEColor(0,0,255,1.0); // blue
var width = 6;
var numSegments = 200;
AddBezierCurve(start, finish, arcHeight, skew, color, width, 'up', numSegments);
}
</script>
</head>
<body onload="MakeMap();">
<div id='myMap' style="position:relative; width:800px; height:600px;"></div>
</body>
</html>
After the HTML <title> tag, I use the <script> element to gain programmatic access to version 6.3 of the Bing Maps AJAX map control library. Notice I’m not using good coding practices, such as including a DOCTYPE declaration, in order to keep the size of my demo code small. At the time I’m writing this article, the current version of the library is version 7. It has improved performance, but it has a completely different code base from earlier versions. Unless you’re using AddBezierCurve with an existing Bing Maps AJAX API set, you should probably use version 7. You should be able to refactor the code I present here for Bing Maps AJAX version 7 with little trouble.
I declare a global VEMap object named map and instantiate it to null. Notice that because JavaScript doesn’t use explicit type declarations, without a comment it’s not obvious that map has type VEMap.
The AddBezierCurve function accepts up to eight parameter values. The first four parameters (start, finish, arcHeight and skew) are required. The next four parameters (color, width, upDown and numSegments) are optional and have default values. The start and finish parameters are type VELatLong (latitude and longitude) objects and represent the endpoints P0 and P2 described in the previous section. The arcHeight parameter represents the height of intermediate shape-defining point P1. The skew parameter represents the left-right adjustment of point P1. The color parameter is the VEColor of the curve. The width is a numeric value that’s the width of the curve. The upDown parameter is a string that can be “up” or “down” and specifies whether the curve should bend up or down. The numSegments parameter specifies the number of values to use for the Bezier t value, which in turn determines how many line segments make up the curve.
The MakeMap function instantiates the VEMap object using the new keyword and sets the control ID to “myMap” so that the Web browser can use the AJAX response to know where on CurveDemo.html to place the map control. I use the LoadMap method to initially position the map centered just north of Denver—with a zoom level of 10—using the Road view. Next I set the start parameter to the latitude and longitude of Boulder and set the finish parameter to an area just north of the Denver International Airport. I set arcHeight to 0.20. As we’ll see in the next section, the arcHeight is interpreted as degrees latitude above the midpoint between P0 (start) and P2 (finish). I set skew to -0.004 to slightly move the hump of the curve 0.004 degrees longitude to the left. I set the color to blue with no alpha transparency, the width of the curved line to 6 and the number of line segments to 200, and then call the AddBezierCurve function with an “up” direction.
In the HTML body tag I use the onload event to call MakeMap, which in turn calls AddBezierCurve.
Defining the AddBezierCurve Function
The AddBezierCurve function begins with:
function AddBezierCurve(start, finish, arcHeight, skew, color, width,
upDown, numSegments)
{
// Preconditions and parameter descriptions here
if (typeof color == 'undefined') { var color = new VEColor(255,0,0,1.0); }
if (typeof width == 'undefined') { var width = 2; }
if (typeof upDown == 'undefined') { var upDown = 'up'; }
if (typeof numSegments == 'undefined') { var numSegments = 10; }
...
To save space, I removed the comments that describe the function’s assumed preconditions (for example, the existence of an instantiated global VEMap object named map) and descriptions of the eight input parameters. The function code begins by defining default parameters. I use the JavaScript typeof operator to determine if parameters color, width, upDown and numSegments are present or not. The typeof operator returns string “undefined,” rather than null, if a variable isn’t present. If the color parameter is absent, I create a local-scope VEColor object named color and instantiate it to red (the parameters to VEColor are red, green, blue and transparency). Using the same approach, I create default values for width (2), upDown (“up”) and numSegments (10).
The function continues:
if (start.Longitude > finish.Longitude) {
var temp = start;
start = finish;
finish = temp;
}
if (numSegments < 2)
numSegments = 2;
...
I normalize the start and finish VELatLong parameters so the start point is to the left of the finish point. Technically this isn’t necessary, but it helps keep the code easier to understand. I perform an error check on the numSegments parameter to ensure that there are at least two line segments for the resulting curve.
Next, I compute the coordinates of a point that lies midway between P0 (start) and P2 (finish) on the line connecting P0 and P2:
var midLat = (finish.Latitude + start.Latitude) / 2.0;
var midLon = (finish.Longitude + start.Longitude) / 2.0;
...
This point will serve as the starting point to construct the intermediate P1 point using the arcHeight and skew parameters.
Next, I determine P1:
if (Math.abs(start.Longitude - finish.Longitude) < 0.0001) {
if (upDown == 'up')
midLon -= arcHeight;
else
midLon += arcHeight;
midLat += skew;
}
else { // 'normal' case, not vertical
if (upDown == 'up')
midLat += arcHeight;
else
midLat -= arcHeight;
midLon += skew;
}
...
I’ll explain the first part of the code logic in a moment. The second part of the branching logic is the usual case where the line connecting start and finish isn’t vertical. In this case I check the value of the upDown parameter and if it’s “up” I add the arcHeight value to the latitude (up-down) value of the midpoint base reference and then add the skew value to the longitude (left-right) of the base reference. If the upDown parameter isn’t “up,” I assume upDown is “down” and subtract arcHeight from the up-down latitude component of the midpoint base reference. Notice that instead of using an explicit upDown parameter, I could eliminate upDown altogether and just infer that positive values of arcHeight mean up and negative values of arcHeight mean down.
The first part of the branching logic deals with vertical or nearly vertical lines. Here I effectively swap the roles of the arcHeight up-down value and skew the left-right value. Notice that there’s no leftRight parameter for the skew; positive values for skew mean to-the-right and negative values mean to-the-left.
With everything ready, I enter the Bezier curve generation algorithm:
var tDelta = 1.0 / numSegments;
var lons = new Array(); // 'x' values
for (t = 0.0; t <= 1.0; t += tDelta) {
var firstTerm = (1.0 - t) * (1.0 - t) * start.Longitude;
var secondTerm = 2.0 * (1.0 - t) * t * midLon;
var thirdTerm = t * t * finish.Longitude;
var B = firstTerm + secondTerm + thirdTerm;
lons.push(B);
}
...
First I compute the difference between t values. Recall from the previous section that t ranges from 0.0 to 1.0 inclusive. So if numSegments = 3, then tDelta would be 0.33 and my t values would be 0.00, 0.33, 0.67 and 1.00, yielding four Bezier points and three line segments. Next, I create a new array named lons to hold the x values that are longitudes. A detailed explanation of the Bezier equations is outside the scope of this article; however, notice that there are three terms that depend on the value of t, P0 (start), P1 (midLon) and P2 (finish). Bezier curves are really interesting, and there’s a lot of information about them available on the Internet.
Next, I use the same Bezier equations to compute the y (latitude) values into an array named lats:
var lats = new Array(); // 'y' values
for (t = 0.0; t <= 1.0; t += tDelta) {
var firstTerm = (1.0 - t) * (1.0 - t) * start.Latitude;
var secondTerm = 2.0 * (1.0 - t) * t * midLat;
var thirdTerm = t * t * finish.Latitude;
var B = firstTerm + secondTerm + thirdTerm;
lats.push(B);
}
...
Now I finish up the AddBezierCurve function:
var points = new Array();
for (i = 0; i < lats.length; ++i) {
points.push(new VELatLong(lats[i], lons[i]));
}
var curve = new VEShape(VEShapeType.Polyline, points);
curve.HideIcon();
curve.SetLineColor(color);
curve.SetLineWidth(width);
map.AddShape(curve);
}
I create an array of VELatLong objects named points and add the latitude-longitude pairs from the lats and lons arrays to the points array. Then I instantiate a VEShape of type Polyline, hide the pesky default icon, set the color and width of the Polyline and use the AddShape method to place the Bezier curve onto the global VEMap object named map.
Testing the AddBezierCurve Function
Testing the AddBezierCurve function isn’t trivial. The function has object parameters (start, finish and color), numeric parameters (arcHeight, skew, width and numSegments) and a string parameter (upDown). In fact, some of my colleagues use similar functions as the basis for job interview questions for software-testing positions. This form of testing is often called API testing or module testing. The first thing to check is basic functionality, or, in other words: Does the function do what it’s supposed to do in more or less normal situations? Next, a good tester would start looking at the function parameters and determine what would happen in the case of bad or edge-case inputs.
The AddBezierCurve function doesn’t perform an initial check on the start and finish VELatLong parameter values. If either or both are null or undefined, the map will render but no curved line will appear. Similarly, the function doesn’t check for illegal values of start or finish. VELatLong objects use the World Geodetic System 1984 (WGS 84) coordinate system, where legal latitudes are in the range [-90.0, +90.0] and legal longitudes are in the range [-180.0, +180.0]. Illegal values can cause AddBezierCurve to produce unexpected and incorrect results. Another bad input possibility for the start and finish parameter values are objects of the wrong type—a VEColor object, for example.
Experienced testers would also test numeric parameters arcHeight and skew in a similar way. Interesting boundary condition values for these parameters include 0.0, -1.0 +1.0, and 1.7976931348623157e+308 (the JavaScript maximum number on many systems). A thorough tester would explore the effects of using string or object values for arcHeight and skew.
Testing the color parameter would be similar to the start and finish parameters. However, you would also want to test the default value by omitting the color parameter in the function call. Note that because JavaScript passes parameters by position, if you omitted the color parameter value you should also omit all subsequent parameter values (width, upDown and numSegments). That said, omitting color but then supplying values for one or more of the trailing parameters would have the effect of misaligning parameter values and is something an experienced tester would examine.
Because the width and numSegments parameters represent physical measurements, good testers would certainly try values of 0 and negative values for width and numSegments. Because these parameter values are implied as integers, you’d want to try non-integer numeric values such as 3.5, too.
If you examine the code for AddBezierCurve, you’ll notice that if the value of the upDown parameter is anything other than “up,” the function logic interprets the parameter value as “down.” This would lead an experienced tester to wonder about the correct behavior of null, empty string and a string consisting of a single space. Additionally, numeric and object values for upDown should be tried. An experienced tester might ask the developer if he had intended the upDown parameter to be case sensitive as it is now; a value of “UP” for upDown would be interpreted as “down.”
API functions are often well-suited for automated testing using random input. You could use one of several techniques to programmatically generate random values for start, finish, arcHeight and skew, send those values to the Web page and see if any exceptions are thrown.
Dual Purposes
Wrapping up, this column has dual purposes. The first is to present a practical JavaScript function to draw a Bezier curve on a Bing Maps AJAX map control along with the underlying code logic. If you ever work with maps, you should find the AddBezierCurve function presented here a very useful resource. The second purpose of the column is to present guidelines for testing a nontrivial JavaScript function. We’ve seen that there are a number of things you should check, including null or missing values, illegal values, boundary values and values with incorrect type. These principles are true for API/module testing functions written in most programming languages.
Dr. James McCaffrey works for Volt Information Sciences Inc., where he manages technical training for software engineers working at the Microsoft Redmond, Wash., campus. He’s worked on several Microsoft products, including Internet Explorer and MSN Search. Dr. McCaffrey is the author of “.NET Test Automation Recipes” (Apress, 2006), and can be reached at jammc@microsoft.com.
Thanks to the following technical experts for reviewing this article: Paul Koch, Dan Liebling, Anne Loomis Thompson and Shane Williams