测试运行

Bing Maps AJAX 的曲线

James McCaffrey

下载代码示例

James McCaffrey
在本月的专栏中,我将介绍一个可用于向 Bing Maps AJAX 地图控件添加曲线的 JavaScript 函数,并讨论用于测试该函数的原则。

要想了解我要介绍的内容,最好查看一下图 1 中的图像。图 1 显示了美国中西部丹佛地区的 Bing Maps AJAX 地图控件。本文的主题是自定义 JavaScript 函数,现在正在使用此函数绘制从科罗拉多大学(我的一个女儿在这里上学)所在地博尔德市到丹佛国际机场(我已经在这里进出过多次了!)的蓝色曲线。该自定义函数被命名为 AddBezierCurve。除了是一个相当有用的函数(如果您曾经使用过 Bing Maps)外,AddBezierCurve 例程还从许多方面演示了 API 测试原则,而且包含一些在其他编程方案中有用的趣味数学。

The Custom Function AddBezierCurve Adds a Curved Line to Bing Maps

图 1 自定义函数 AddBezierCurve 向 Bing Maps 添加曲线

在下面几节中,我将先为您简要介绍一下如何创建贝塞尔曲线,这是 AddBezierCurve 使用的基本数学方法。接下来,我将逐行介绍此函数,以便您能够根据自己的需要修改源代码(如有必要)。最后,我将介绍可用于测试 AddBezierCurve 和类似的 JavaScript 函数的一般原则和特定方法。本文假定您已基本熟悉 JavaScript,但以前未使用过 Bing Maps AJAX 控件库进行编程。我想,您一定会发现本文介绍的主题非常有趣,而且对于提高您的开发人员和测试人员的各种技能非常有用。

贝塞尔曲线

贝塞尔曲线可用于在两点之间绘制曲线。尽管贝塞尔曲线有许多变体,但最简单的形式还是二次贝塞尔曲线,它需要两个端点(通常称作 P0 和 P2)和一个中点 (P1) 来决定曲线的形状。请看图 2 中的 XY 图形示例。

Constructing Bezier Curves

图 2 绘制贝塞尔曲线

点 P0、P1 和 P2 在图形上是空心的红色圆圈。两个端点是 P0 = (1,2) 和 P2 = (13,8)。中点是 P1 = (7,10)。贝塞尔函数接受范围在 0.0 到 1.0 的参数(通常称作 t)。t 的每个值均可在曲线上 P0 和 P2 之间产生一个点。

图 2 中,我使用了 5 个 t 值:0.0、0.25、0.50、0.75 和 1.00。这产生了 5 个点,即图形上显示的小点。我将提供在探讨 AddBezierCurve 函数的代码时使用的等式。您可以看到 t = 0.0 时的第一个贝塞尔点为 (1,2) = P0,t = 1.0 时的最后一个贝塞尔点为 (13,8) = P2。t = 0.0 和 t = 1.0 的值通常会生成点 P0 和 P2。如果将贝塞尔点用线段连接起来,则大致可以在 P0 和 P2 之间绘制成一条优美的曲线。t 的值越多,生成的点就越多,这样生成的近似曲线就会更加平滑。

如果给定了端点 P0 和 P2,则可通过 P1 的值确定得到的贝塞尔曲线的形状。在图 2 的示例中,我随意在横轴上 P0 与 P2 中间以及在纵轴上略高于最高点 (P2) 的位置选取了一个点。如果我将 P1 向左挪一点儿,曲线就会向左移动,而且会变得更加对称。如果我增加 P1 的高度,则曲线就会更高且更尖。

调用 AddBezierCurve 函数

图 1 中,您可以看到我的地图在一个名为 CurveDemo.html 的网页(可在可下载代码中找到)上。图 3 显示了 CurveDemo.html 的整体结构。

图 3 演示网页结构

<html>
<!-- CurveDemo.html -->
<head>
<title>Bing Maps AJAX Bezier Curve</title>
<script type="text/javascript"
 src="http://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>

在 HTML <title> 标记后面,我使用 <script> 元素来获取对 Bing Maps AJAX 地图控件库 6.3 版的编程访问权限。 请注意,我并没有为了保持简短的演示代码而采用更好的编码方法,如包括 DOCTYPE 声明。 在我编写这篇文章时,库的当前版本是第 7 版。 该版本的性能得到了提高,但与早期版本相比,却具有截然不同的代码库。 除非使用带有现有 Bing Maps AJAX API 集的 AddBezierCurve,否则您可能应该使用第 7 版。 您应该能够轻而易举地重构我在这里介绍的针对 Bing Maps AJAX 第 7 版的代码。

我声明一个名为 map 的全局 VEMap 对象并将其实例化为 null。 请注意,因为 JavaScript 不使用显式类型声明,所以在没有注释的情况下无法明显地看出地图是否属于 VEMap 类型。

AddBezierCurve 函数最多可接受 8 个参数值。 前 4 个参数(start、finish、arcHeight 和 skew)是必需的。 后 4 个参数(color、width、upDown 和 numSegments)可选且具有默认值。 start 和 finish 参数是 VELatLong 类型(纬度和经度)对象,并且代表上一节所述的端点 P0 和 P2。 arcHeight 参数代表定义形状的中点 P1 的高度。 skew 参数代表点 P1 的左右调整。 color 参数是曲线的 VEColor。 width 是曲线宽度的数字值。 upDown 参数是一个字符串(该字符串可以是“up”,也可以是“down”),并指定曲线将向上还是向下弯曲。 numSegments 参数指定用于贝塞尔 t 值的值数量,该值数量反过来决定构成曲线的线段数量。

MakeMap 函数使用新的关键字实例化 VEMap 对象,并将控件 ID 设置为“myMap”,以便 Web 浏览器可以使用 AJAX 响应来了解地图控件在 CurveDemo.html 上的存放位置。 我使用 LoadMap 方法和“Road”(道路)视图首先将地图的中心定位在丹佛的正北(缩放比例为 10)。 接下来,我将 start 参数设置为博尔德的纬度和经度,将 finish 参数设置为丹佛国际机场的正北地区。 我将 arcHeight 设置为 0.20。 我们将在下一节看到,arcHeight 被解释为高于 P0 (start) 和 P2 (finish) 之间中点的纬度。 我将 skew 设置为 -0.004,以便稍微将曲线顶点的经度向左移动 0.004 度。 我将颜色设置为蓝色(非 alpha 透明),将曲线的宽度设置为 6,将线段数设置为 200,然后调用 AddBezierCurve 函数,方向是“up”。

在 HTML 正文标记中,我使用 onload 事件调用 MakeMap,MakeMap 又调用 AddBezierCurve。

定义 AddBezierCurve 函数

AddBezierCurve 函数的开头是:

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; }
  ...

为了节省空间,我删除了描述函数的假定前置条件(例如,存在名为 map 的实例化的全局 VEMap 对象)的注释以及对 8 个输入参数的描述。 函数代码先定义默认参数。 我使用 JavaScript typeof 运算符来确定是否存在 color、width、upDown 和 numSegments 参数。 如果不存在变量,则 typeof 运算符返回字符串“undefined”,而不是 null。 如果 color 参数不存在,我会创建一个名为 color 的本地范围 VEColor 对象,并将其实例化为 red(VEColor 的参数包括 red、green、blue 和 transparency)。 我使用同样的方法创建默认值:width (2)、upDown(“up”)和 numSegments (10)。

该函数的后续代码如下:

if (start.Longitude > finish.Longitude) {   
 var temp = start;
 start = finish;
 finish = temp;
}

if (numSegments < 2)
 numSegments = 2;
...

我将 start 和 finish VELatLong 参数规范化,以便使起点位于终点的左侧。 从技术上讲,没有必要这样做,但这有助于使代码更易于理解。 我对 numSegments 参数进行错误检查,以确保得出的曲线至少有两条线段。

接着,我计算在连接 P0 (start) 和 P2 (finish) 的直线上位于 P0 和 P2 中间的点的坐标:

var midLat = (finish.Latitude + start.Latitude) / 2.0;
var midLon = (finish.Longitude + start.Longitude) / 2.0;
...

此点将充当使用 arcHeight 和 skew 参数绘制中点 P1 的起点。

下面,我确定 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;
}
...

稍后,我将解释代码逻辑的第一部分。 分支逻辑的第二部分用于处理连接起点和终点的线不是竖线的常见情况。 在这种情况下,我会检查 upDown 参数的值,如果是“up”,我会向中点基本引用的纬度(上-下)值添加 arcHeight 值,然后向该基本引用的经度(左-右)添加 skew 值。 如果 upDown 参数不是“up”,则假定 upDown 是“down”,并从中点基本引用的上-下纬度分量中减去 arcHeight。 请注意,我不使用显式 upDown 参数,而是完全消除 upDown,而且只是推断:arcHeight 如果是正值,则意味着 up,arcHeight 如果是负值,则意味着 down。

分支逻辑的第一部分用于处理竖线或接近竖直的线。 我在这里实际上对 arcHeight 上-下值的角色进行了互换,而且使左-右值产生一些偏差。 请注意,偏差没有 leftRight 参数;偏差为正值表示偏右,偏差为负值表示偏左。

一切就绪后,我便输入贝塞尔曲线生成算法:

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);
}
...

首先,计算 t 值之间的区别。 回想一下,在上一节中我提到 t 的范围是 0.0 到 1.0(包括 0.0 和 1.0)。 因此,如果 numSegments = 3,则 tDelta 就是 0.33,我的 t 值就是 0.00、0.33、0.67 和 1.00,且会生成 4 个贝塞尔点和 3 条线段。 接下来,我创建一个名为 lons 的新数组,以保留表示经度的 x 值。 对贝塞尔等式的详细解释不在本文的范围之内;但请注意,有 3 个术语取决于 t 的值,即 P0 (start)、P1 (midLon) 和 P2 (finish)。 贝塞尔曲线确实很有趣,而且 Internet 上也有许多关于它们的信息。

接下来,我使用同样的贝塞尔等式将 y(纬度)值计入一个名为 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);
}
...

现在,我将在 AddBezierCurve 函数的结尾使用下面的代码:

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);
}

我创建了名为 points 的 VELatLong 对象数组,并将 lats 和 lons 数组中的纬度-经度对添加到该 points 数组。 然后,我实例化 Polyline 类型的 VEShape,隐藏令人讨厌的默认图标,设置 Polyline 的颜色和宽度,并使用 AddShape 方法将贝塞尔曲线放置到名为 map 的全局 VEMap 对象上。

测试 AddBezierCurve 函数

测试 AddBezierCurve 函数并不是件容易的事。 该函数拥有对象参数(start、finish 和 color)、数值参数(arcHeight、skew、width 和 numSegments)和字符串参数 (upDown)。 实际上,我的一些同事在面试一些应聘软件测试职位的候选人时,常会根据类似函数来出求职面试题。 这种测试形式通常被称作 API 测试或模块测试。 首先要检查的是基本功能,或换句话说:该函数在基本正常的情况下是否可以发挥应有的作用? 接下来,优秀的测试人员应开始查看该函数的参数,并确定在出现错误输入或边缘情况输入时会发生什么。

AddBezierCurve 函数不对 VELatLong 参数值 start 和 finish 执行初始检查。 如果这两个参数值有一个是或二者都是 null 或 undefined,则地图将会呈现,但不会显示曲线。 同样,该函数不检查 start 或 finish 值是否非法。 VELatLong 对象使用 World Geodetic System 1984 (WGS 84) 坐标系,其中合法纬度的范围是 [-90.0, +90.0],合法经度的范围是 [-180.0, +180.0]。 非法值可导致 AddBezierCurve 产生意外和不正确的结果。 start 和 finish 参数值的另一种错误输入的可能是输入错误类型的对象(例如,VEColor 对象)。

有经验的测试人员还会以类似的方式测试数值参数 arcHeight 和 skew。 这些参数的有趣边界条件值包括 0.0、-1.0 +1.0 和 1.7976931348623157e+308(许多系统上使用的 JavaScript 最大值)。 认真的测试人员会探讨使用 arcHeight 和 skew 的字符串值或对象值的效果。

测试 color 参数类似于测试 start 和 finish 参数。 不过,您还需要通过忽略函数调用中的 color 参数来测试默认值。 请注意,因为 JavaScript 是按位置传递参数的,所以如果忽略 color 参数值,您还应忽略所有后续参数值(width、upDown 和 numSegments)。 话虽如此,忽略 color 但接着又为一个或多个后缀参数提供值,这样会导致参数值不一致,对于这种情况,经验丰富的测试人员会进行检查。

因为 width 和 numSegments 参数代表物理测量,所以优秀的测试人员自然会尝试对 width 和 numSegments 采用 0 值和负值。 因为已经可以知道这些参数值是整数,所以您还需要尝试非整数数值,如 3.5。

如果您检查 AddBezierCurve 的代码,则会注意到,如果 upDown 参数的值是“up”之外的任何值,则函数逻辑会将该参数值视为“down”。这会使经验丰富的测试人员对 null、空字符串和由单个空格组成的字符串的正确行为感到疑惑。 另外,还应尝试 upDown 的数字值和对象值。 经验丰富的测试人员还会询问开发人员是否有意将 upDown 参数设置为区分大小写(如现在的形式);是否将 upDown 的“UP”值解释为“down”。

API 函数通常非常适用于通过随机输入进行的自动测试。 您可以使用几种方法之一,以编程方式生成 start、finish、arcHeight 和 skew 的随机值,将这些值发送到网页,并查看是否会抛出异常。

双重目的

总而言之,本专栏具有双重目的。 第一个目的是介绍用于在 Bing Maps AJAX 地图控件上绘制贝塞尔曲线的实际 JavaScript 函数及基本的代码逻辑。 如果您曾经使用过地图,则应发现本文介绍的 AddBezierCurve 函数是一种非常有用的资源。 本专栏的第二个目的是介绍用于测试重要的 JavaScript 函数的指导原则。 我们看到,有许多方面应该检查,包括 null 值或缺少的值、非法值、边界值和类型错误的值。 对于采用大多数编程语言编写的 API/模块测试函数,这些原则都适用。

James McCaffrey博士 供职于 Volt Information Sciences, Inc.,在该公司他负责管理对华盛顿州雷蒙德市沃什湾 Microsoft 总部园区的软件工程师进行的技术培训。他参与过多项 Microsoft 产品的研发工作,包括 Internet Explorer 和 MSN Search。McCaffrey 博士是《.NET 软件测试自动化之道》(Apress, 2006) 的作者,您可通过以下电子邮箱地址与他联系:jammc@microsoft.com

衷心感谢以下技术专家对本文的审阅:Paul Koch、Dan Liebling、Anne Loomis ThompsonShane Williams