1. 引言
随着互联网技术的快速发展,在客户端进行数据的图形化展示与交互也受到越来越多的关注。其中,WebGIS [1] 中的地理要素的可视化显示,也需要在客户端对点、线、面、以及复杂地理要素进行绘制与编辑等交互操作。现有WebGIS系统中在客户端实现矢量绘图技术根据其底层实现机制,主要分为两类[2] :一种是外部代码载入方式如Flash、Applet、SilverLight等,一种是浏览器原生支持的方式如Canvas、SVG、VML等。第一种方式因为要导入外部执行环境,通常要安装插件,而在浏览器禁用插件后,相关的功能也会受到影响,有一定风险。相比较而言,后一种方式则直接由浏览器原生支持,但在浏览器支持性以及绘图效率等方面也有较大差异。
VML (Vector Markup Language,矢量可标记语言)是一种基于XML描述的矢量图形,支持高质量的图形显示。在WebGIS客户端引入VML,可以弥补WebGIS中绘图的诸多不足,它作为IE浏览器内置的绘制工具,无需任何额外的组件,不仅提高了开发进度,而且增强了系统的适用性[3] 。但它仅被IE支持,存在很大的兼容性问题,并且在高版本的IE浏览器(IE10)中也逐渐淡化VML,开始使用SVG替代VML进行矢量绘图。
SVG指可缩放矢量图形(Scalable Vector Graphics),也是基于XML的,支持DOM事件模型,具有良好的交互性。但在绘制对象数量较多时,DOM操作会严重影响性能。并且它不支持绘制png、jpg等格式的图片,绘制的图形也无法导出成图片。
HTML5 Canvas [4] 是W3C推出的新一代浏览器端绘图API,它是基于像素级的绘图技术,支持图像像素操作,能够绘制路径和栅格图像,绘制后的对象可以转成base64编码,在前端保存为png、jpg图片。目前,主流浏览器都支持Canvas,IE8之前的版本也可通过ecxcanvas.js插件实现。许多客户端图表也是基于Canvas实现的[5] [6] ,如一些插件echarts、CanvasXPress、AwesomeChartJS、RGraph等都是基于Canvas实现的。而且随着HTML5标准的规范化,HTML5技术日趋成熟,使用Canvas进行前端矢量绘图也成为一种趋势。
Canvas除了用于基本的图形展示外,还可用于WebGIS中空间要素的显示与表达 [7] ,实现地图数据的显示和交互,尤其是在要素量较大的情况,绘制效率远高于SVG。对基本的点、线、面的表达,Canvas API中提供了相关的接口,但对复杂地理要素诸如多线、多面、岛洞多边形等并没有提供绘图接口。而实际的地理对象中并不只是单一的几何对象,有时会出现一些复杂的地理对形的嵌套,形成岛、洞等复杂对象。文中针对这类复杂地理要素的绘制给出解决方案,并对其性能进行分析。
2. HTML5 Canvas API简介
Canvas是HTML5中新增加的标签元素,用于客户端绘图,其绘图能力依赖于JavaScript脚本。它定义了一个矩形画布,通过画布元素的context对象,调用一系列的绘图方法,主要包括:moveTo、lineTo、stroke、fill、arc、quadraticCurveTo等绘制路径的方法,以及drawImage绘制图像的方法,该方法可接受img、canvas,video对象等作为参数。除了绘制方法外,还包括一些坐标变换、样式设置、像素级别的操作方法。
Canvas不仅支持绘制简单图形如线、矩形、圆、扇形等,还支持绘制复杂的曲线如二次贝塞尔曲线、三次贝塞尔曲线。除了支持矢量要素的绘制,还可用于栅格图像的绘制。目前,现代浏览器如Internet Explorer 9、Firefox、Opera、Chrome 以及Safari都已支持 <canvas> 及其属性和方法。
3. 要素绘制的基本方法
3.1. 点要素的绘制
点要素的绘制实际上就是圆的绘制,调用Canvas API [8] 中的arc方法,以鼠标点击位置为圆心,设定半径以及填充样式,绘制圆即可,代码如下:
ctx=document.getElementById(“myCan”).getContext(“2d”);
ctx.arc(pt.x, pt.y, 4, 0, Math.PI * 2, true);
ctx.fill(); //绘制半径为4的圆
3.2. 线要素的绘制
绘制线要素就是不断地调用lineTo方法,首先,在鼠标单击下第一点时,通过moveTo绘制线段起点,之后每次单击的位置点作为线段的下一拐点,通过lineTo绘制两点间的线段,直至双击结束,完成该线要素的绘制。
ctx.moveTo(sxy0.x, sxy0.y);
for (var i = 1; i < ptArr.length; i++) {
ctx.lineTo(ptArr[i].x, ptArr[i].y); }
ctx.stroke();
//ctx.closePath(); //ctx.fill(); }
3.3. 面要素的绘制
Canvas API中没有提供绘制多边形的方法,只有绘制矩形的drawRect方法,而多边形的绘制和绘制线要素基本一致,不同的是,线要素绘制结束后使用stroke描边,而面要素需使用closePath闭合和fill方法进行填充。
3.4. 岛洞要素的绘制
要素绘制过程中,采用的是双层画布,在临时画布上绘制的都是简单多边形,双击结束后,将临时画布上的要素绘制到要素层画布featureCanvas上,在该画布上的绘制会涉及到复杂的岛洞多边形。因此,在将临时画布上的要素向要素层上绘制时,还需要进一步判断当前绘制的多边形与要素层上已存在的多边形的位置关系。如果当前多边形与已有的多边形不存在包含关系,则作为简单多边形处理;否则的话,则会形成岛或者洞多边形如图1,具体地,需要根据两个多边形的方向进行判断。如果两者的方向一致,
即同为逆时针或顺时针,则形成岛;否则形成洞多边形。
具体过程:在完成一个简单多边形的绘制后,将当前多边形与要素层上的已有多边形要素逐一进行多边形在多边形内的判断(代码见下)。如果存在包含关系,即为岛洞多边形,需要将两个多边形合并成一个新的要素,主要是将其中的坐标信息_lineRings合并,重组成一个新的多边形要素,并从内存数组和要素层中移除原来要素,流程如图2。
//判断多边形在多边形内
function isPolyInPoly(poly,outerPoly){
for(var i=0;i
var pt1 = poly[i];
var pt2 = poly[i+1];
var line = [pt1,pt2];
var isInpoly1 = pointInPoly(pt1,outerPoly);
var isInpoly2 = pointInPoly(pt2,outerPoly);
//两端点不都在多边形内
if(!isInpoly1 || !isInpoly2)return false;
//判断每条边是否在多边形内
var flag = isLineInPoly(line,outerPoly);
if(!flag) return false;}
return true; }
要素的空间信息都存储在lineRings二维数组中,具体的组织方式如下:
1) 简单多边形的空间信息lineRings:[ptArr],其中ptArr是多边形顶点坐标组成的数组。
2) 复杂多边形的空间信息lineRings:[ptArr1,ptArr2,....ptArr i....],ptArr1是外部多边形的顶点坐标数组,ptArr2...ptArri是内部的岛或洞的顶点坐标数组。
在将临时画布上的简单要素或合并后的复杂要素向要素层上绘制要素时,由于存在岛洞的情况,需要根据每类要素的特点采取不同的绘制策略,具体如表1。
绘制岛洞多边形时,首先,需要判断这个多边形要素的各部分的包含关系,即多边形在多边形内的判断,确定其包含层级关系,然后通过isClockWise方法判断多边形的方向,是顺时针还是逆时针方向;比较存在包含关系的两多边形的方向是否一致,如果方向不一致,则形成一个洞,绘制时将这个内部多边形的填充色设为白色,形成一个空白区域。否则的话,这个内部多边形就是一个岛,绘制时只绘制边框。具体绘制过程如下:
// 岛洞多边形的绘制
var dir_outer = isClockWise(lineRings[0].points);
for(var i=1;i
var pointArr=lineRings[i].points;
var dir_inner=isClockWise(pointArr);
if((dir_outer&&!dir_inner)||(!dir_outer&&dir_inner)){
holeOrIsland = 'hole'; //方向不一致
}else{ holeOrIsland = 'island';}
drawPoly_inner(ctx,pointArr,holeOrIsland );
}
4. 要素交互式绘制与内存模型
4.1. 交互式绘制技术
Canvas绘图是基于位图的,按照像素单位在画布上进行绘制,绘制的内容保存在Canvas对象中。Canvas整体作为一个对象,支持DOM事件模型,但是Canvas中每个图形则无法直接通过DOM事件进行交互。因此,在对画布上的图形进行交互时,需要通过鼠标相关事件模拟交互过程。图3是以绘制多边形为例,作以说明绘制过程中的鼠标事件。
首先,声明两个相同宽高的画布图层,使用绝对定位叠放在一起,一个用于显示绘制好的图形(featureCanvas),一个用于显示绘制过程中的图形的变化(overlayCanvas)。使用两个画布的目的在于提高绘制效率,在绘制过程中,鼠标每移动一次就需要重绘一次,如果在同一个画布上,则需要反复清空画

Figure 2. Workflow of drawing complex features
图2. 绘制复杂要素的流程

Table 1. Strategies of drawing diverse features
表1. 各种类型的要素绘制策略
布再重绘,图形数量较多时,重绘会降低性能。使用双层画布,将当前绘制的图形放在临时画布上,已经绘制好的放在另一画布上,在绘制过程中,只需要重绘临时画布上的内容即可,能够大大提升性能。
4.2. 要素内存模型
在前后台的通讯过程中,主要通过实验室自主研发的Geoserver2013提供的要素服务接口进行要素的增删改查操作,除了要素服务外,同时它还提供了切片地图服务、动态图服务、文件服务、数据库服务等接口。在前端绘制的要素,使用gFeature对象存储,通过Geoserver提供的要素服务接口,将其提交至服务器进行保存;前端也可以通过属性查询和几何查询向服务器端请求要素集,以Geojson格式返回,在前端处理成gFeature格式,并绘制显示。
WebGIS中的矢量要素通常包含空间和属性两大部分,空间数据主要描述地理对象的位置,属性描述该地理对象的特征信息。在前端存储要素数据时,将要素的两部分信息分别存储在gFeature对象中,空间信息主要存储在shape中,属性信息存储在fields中,具体如图4。
5. 要素绘制性能分析
在要素绘制过程中不可避免地会涉及到绘制性能问题,影响其性能的因素众多,如要素的数量、要
素类型、绘制样式、浏览器类型以及硬件配置等。本文主从以下三个方面:不同数量、不同绘制样式、以及不同浏览器来对比Canvas绘制性能。
5.1. 基本性能分析
以绘制最简单的多边形为例,测试在同一浏览器中,以相同的绘制样式绘制不同数量的多边形耗费的时间。先声明一个1000 × 600的画布,利用随机函数在画布内生成含有三个结点的多边形,对比绘制不同数量的相同结点数的多边形的耗时。在测试过程中,多边形的绘制样式统一设置了填充色、边框色、线宽、边角样式等,测试使用的浏览器是FireFox33。由于单次测试存在一定偶然性,因此,绘制耗时的计算采用的是取100次测试结果的平均值,最后,得出不同绘制规模下的平均绘制时间(如表2),绘制时间和绘制数量成线性关系。
要素绘制性能不仅与要素的数目有关,还与每个要素的复杂程度有关,相同数目的要素,要素结构越复杂,即构成每个要素的结点数目越多,绘制时就越耗时。在测试实验中,分别对比同等数量下,三结点和五结点构成的多边形的绘制耗时。结果发现,两者的绘制时间存在一个倍数关系,同等数量下,绘制五边形的时间是三角形的2.26倍左右。
5.2. 绘制样式对性能的影响
进行要素绘制时,不仅可以使用颜色填充,也可以使用图像填充。但是二者的填充效率如何,需要进一步测试。分别比较使用纯色填充和图片填充的绘制效率,其中,图片大小为3 KB,图片填充样式的设置如下:
var fillImg = new Image();
fillImg .src = 'apple.png';
var pstyle=ctx.createPattern(fillImg ,'repeat');
ctx.fillStyle=pstyle;
图5是对比绘制不同规模数量的三角形,分别使用两种填充方式所需要的时间,从表中可看出,图像填充比较耗时,约是纯色填充耗时的4倍左右。
5.3. 不同浏览器中绘制效率对比
不同浏览器中绘制效率对比Canvas是由浏览器解析并处理的,因此,其绘图的性能与浏览器平台有

Table 2. Time consuming of drawing different numbers of features
表2. 绘制不同数量要素的耗时对比
直接关系,与客户端的硬件设备性能也有间接关系。不同浏览器对JavaScript的解析与执行速度会有所差异,这也会影响Canvas的绘制性能。部分浏览器开发商实现了Canvas GPU硬件加速,会大大加快了绘制速度。
下面的测试实验是在启用了浏览器的GPU加速功能后,对比三类浏览器中的绘制效率。分别在Chrome43、FireFox33、IE11三个浏览器中,绘制同等数量的三角形,都采用纯色填充的方式,对比要素的绘制效率,测试结果如下所示,从图6中可看出,Chrome中JavaScript的执行效率是最高的,其次是FireFox,IE最慢。各浏览器JS的执行速度还存在一定的数量关系,Chrome中JS的执行速度是Firefox的4~5倍,是IE的6~8倍。
6. 要素绘制性能分析
6.1. 性能分析
在绘制过程中,要素数量、结点数、填充样式等都会影响绘制性能,而具体地这些因素是如何影响性能的,还需要进一步探讨。要找出绘制过程中时间消耗在哪些操作中,可以借助Chrome提供的Timeline工具,它会将web应用过程中各部分的耗时概况显示出来,主要包括DOM事件的处理、页面布局渲染、向屏幕绘制元素三个层面上的耗时数据,帮助找出耗时操作。
分别对比绘制5000个要素时,启动Chrome的Timeline模块,记录下绘制过程中的具体耗时概况,结果如图7。其中,黄色部分代表分析和执行JavaScript所用的时间,紫色表示计算元素样式和布局所用

Figure 5. Time consuming comparison in different styles
图5. 不同绘制样式的耗时对比

Figure 6. Canvas rendering time consuming in different browsers
图6. 不同浏览器中Canvas绘制耗时对比
时间,绿色是绘制屏幕所用时间,在三者的耗时中,脚本运算耗时最多,占比最大,而且随着要素数量的增加,脚本运算耗时几乎成倍增加。由此可见,绘制过程中,脚本运算对性能影响最大。
Profile模块提供了对脚本中各函数执行时间的统计,可以帮助网站找出哪些函数导致的脚本阻塞。利用该模块下的JavaScript CPU Profile来记录测试页面中的JavaScript各函数的执行时间,从中找出最耗时的操作。在性能优化时,考虑最大程度的减少这些耗时操作。表3是分别绘制1000个和5000个要素时,脚本各函数运行时间的记录。其中,当绘制5000个要素时,描边操作耗费了10.9 ms,填充、画线、移动节点分别消耗4 ms左右。在绘制过程中,绘制时间主要消耗在画线、描边、填充等操作上。
通过上述分析可以看出,stroke、fill、lineTo、moveTo等函数的执行耗时占比较大,这也说明了要素数量、结点数以及填充样式会对要素的绘制时间产生影响。在随着要素数量增多时,这几个耗时函数的调用次数都会增多,导致最终的耗时增加;同等数量的要素,要素结点数越多,会使 moveTo、lineTo函数的调用次数增加,从而增加绘制时间;填充样式的复杂程度会影响fill函数的执行时间,最终影响绘制时间。因此,绘制性能的提升主要体现在对这些操作的优化,如何最大程度地较少耗时操作。
6.2. 优化方法
6.2.1. 使用双层画布
在绘制过程中,每进行一次鼠标操作,就需要执行一次重绘,以保证最新的绘图状态。当画布比较大,且画布上的要素比较多时,重绘操作会阻塞线程,出现卡的现象,影响用户体验。采取的解决办法是使用双层画布,将已绘制好的要素放置在一个目标画布上,而正在绘制或修改的要素放在一个覆盖层画布上,当绘制完成或修改完成后,一般是在鼠标触发MouseUp时,再将覆盖层画布上的要素绘制到目标画布上,更新目标画布的视图状态。
6.2.2. 预渲染
使用离屏Canvas,声明一个不可见的Canvas画布,同目标画布同等大小,预先将要素绘制到离屏画

Figure 7. Time consuming of drawing 5000 features
图7. 绘制5000个要素的耗时

Table 3. Executing time of drawing functions
表3. 绘制函数的执行时间
布上,在需要的时候,通过drawImage方法把离屏画布中的内容绘制到目标画布中。
function preRender(){ //将离屏mCanvas中的内容绘制到目标画布
ctx.drawImage(mCavans,0,0,mWid,mHei);
}
6.2.3. 请求视窗范围内的地理要素
在绘制地理要素时,地理范围可能会很大,可以先根据地理坐标与屏幕坐标的换算公式,确定当前画布视窗范围内能够显示的地理要素。在执行修改要素、绘制要素时,只涉及到当前视窗范围内的要素状态变化,减少重绘操作的性能损耗。
6.2.4. 函数节流
当画布尺寸发生变化时,视图范围随之变化,视窗范围内的要素需要重新计算,并执行要素的重绘操作。但如果画布尺寸变化事件不断触发,如在改变浏览器窗口大小时,则会频繁执行重绘操作,带来严重的性能损耗。为了避免这种情况的发生,可以采用函数节流的方式,让重绘操作只在超过指定的时间间隔后才执行[9] ,如指定在500毫秒后再调用画布尺寸变化的处理函数,避免画布尺寸频繁变化时,连续触发响应函数。
function throttle(method,context){
clearTimeout(method.timerId);
method.timerId=setTimeout(function(){
method.call(context);},500); }
7. 要素绘制性能分析
在《上海市农业布局规划动态管理系统》中(图8),依托上海市高清遥感影像,对上海全市农用地数据进行在线采集[10] 。其中,农用地地块数据主要采用了Canvas进行绘制,并通过fillText方法绘制地块类型的文字。在性能方面,采用了双层画布绘制、控制视窗范围内的要素显示以及函数节流等进行优化,缩放平移操作中,要素响应基本在1秒内;在并发访问上,可以满足不少于20个用户的同时在线数据编辑。

Figure 8. The planning system of Shanghai agriculture
图8. 上海市农业布局规划系统
实验室自主研发的ECNUGIS [11] 平台(图9)是一个开放的数据管理与分享平台,提供了在线创建要素集,进行要素的绘制与编辑以及数据分享的功能。其中,要素的绘制与编辑使用的也是Canvas,用户可以自主创建要素集,进行在线矢量化,并通过实验室研发的Geoserver服务器将要素集保存在后台,通过要素服务和动态图服务供用户后续调用。
8. 结论
文中对前端使用Canvas进行地理要素的绘制进行了探讨,针对Canvas交互做了鼠标事件模型,实现了复杂岛洞多边形要素的绘制,为WebGIS中复杂地理要素的可视化显示以及要素编辑提供了基础。在性能方面,主要从要素数量、要素结点数、绘制样式、浏览器类型四个方面进行了要素绘制耗时的对比试验,并进一步提供了在WebGIS中绘图时的几种优化方法。
HTML5 Canvas客户端绘图技术越来越成为一种趋势,尤其是针对这种数据量大、比较零碎的地理对象的绘制与展示,而且随着HTML5规范的标准化,Canvas的浏览器支持性会更好。此外,Canvas元素除了支持2D [12] 绘图,还支持3D绘制,这主要赖于浏览器的支持,随着浏览器的不断迭代更新,未来直接通过Canvas在浏览器中实现3D绘图也变得十分有可能。