Monthly Archives: December 2010

ArcGIS客户端API中另一种图层类型的探讨:DynamicTileMapServiceLayer

    ArcGIS客户端API(Javascript/Flex/Silverlight)中,我们最常打交道的是ArcGISDynamicMapServiceLayer和ArcGISTiledMapServiceLayer两个类,基本每个地图中都要用到。它们都可以直接将服务器端发布的地图服务(MapService)作为图层,加载到客户端程序中,分别对应了动态地图服务和缓存地图服务。这两种图层类型各有优缺点。
    ArcGISDynamicMapServiceLayer(动态地图服务)通常用于实时显示经常变化的数据,支持控制单个图层可见性,可动态投影。但缺点是显示效果较差,整个服务出图较慢;ArcGISTiledMapServiceLayer可以直接加载服务器端的缓存地图服务,显示效果好,速度快,但它的缺点正是ArcGISDynamicMapServiceLayer的优点,即不支持动态投影,不能控制图层可见性,服务器端需要提前生成缓存等。
    这里尝试自己来在客户端封装一个类,创建一种新的客户端图层类型。它能够综合以上两个图层的优点,而克服其各自的缺点。大致总结一下我们要达到的目的:

ArcGISDynamicMap

ServiceLayer

ArcGISTiledMap

ServiceLayer

自定义的客户端图层

实时获取最新数据

Y

N

Y

切片方式显示服务

N

Y

Y

需要提前生成缓存

Y

N

客户端缓存切片加快显示速度

N

Y

Y

利用subdomain加速缓存加载

N

Y

Y

支持动态投影

Y

N

Y

动态指定图像输出格式

Y

N

Y

控制子图层可见性

Y

N

Y

利用LayerDefination过滤数据

Y

N

Y

利用TimeExtent显示时态数据

Y

N

Y

其它缓存服务特性

N

Y

Y

其它动态服务特性

Y

N

Y

 
    由于这种图层类型三种客户端API(Javascript/Flex/Silverlight)均可使用,因此在这里我们就不讨论具体的实现代码,只说明一下实现思路。
    首先来解决以缓存服务的方式来显示动态服务的问题。《ArcGIS客户端API中加载大量数据的几种解决办法(以Silverlight API为例)》一文中其实已经提到,主要是继承TiledMapServiceLayer,其中获取切片的GetUrl()方法,返回值是利用REST SDK中的ExportMap拼接的Url。这样,我们就并不需要提前切图,输入动态地图服务,从而达到缓存地图服务的显示效果。
    举一个例子,如果我想让自己的服务可以和Google Maps/Bing Maps/ArcGIS Online的服务相叠加(WKID 102100/3857),那么GetUrl()方法中看其来应该是这样(里面包括了如何根据level,row,col来计算一个切片的四个角点坐标):
   1:  public override string GetTileUrl(int level, int row, int col)
   2:          {
   3:              string baseUrl = @"{0}/export?dpi=96&transparent=true&format=png8&bbox={1}%2C{2}%2C{3}%2C{4}&bboxSR=102100&imageSR=102100&size=256%2C256&f=image";
   4:   
   5:              double cornerCoordinate = 20037508.3427892;
   6:              double originResolution = cornerCoordinate * 2 / 256;
   7:              double resolution = originResolution;
   8:              for (int i = 0; i < level; i++)
   9:              {
  10:                  resolution /= 2;
  11:              }
  12:              double xmin, ymin, xmax, ymax;
  13:              //double resolution = 39135.7584820001;
  14:              xmin = -cornerCoordinate + resolution * 256 * col;
  15:              ymin = cornerCoordinate - resolution * 256 * (row + 1);
  16:              xmax = -cornerCoordinate + resolution * 256 * (col + 1);
  17:              ymax = cornerCoordinate - resolution * 256 * row;
  18:   
  19:              return string.Format(baseUrl, Url, xmin, ymin, xmax, ymax);
  20:          }

    然后再来看下我们自定义图层实现ArcGISDynamicMapServiceLayer功能的可行性。

    关于动态投影。如果想要叠加到WGS84坐标的底图上,只需要将bboxSR和imageSR改成4326即可。这样便支持了动态投影。

    关于“切片”格式ImageFormat。只需修改上面baseUrl中的format参数。这样一来既不需要在服务器端提前切图,也能动态改变“切片”的格式。

    关于服务加载速度。如果对于出图速度不满意,则可以在服务器端发布若干个相同的服务,轮询使用每个服务来出图,可以达到并行加速的目的。

    关于DisableClientCache。默认情况下,与ArcGISTiledMapServiceLayer一样,这些“切片”会缓存在客户端,便于再次浏览。如果服务器端数据有变,那么就无法看到最新的变化情况,这是缓存服务的一个缺点。在自定义的图层类型中,我们可以在exportmap操作的Url最后再加一个时间戳参数,比如_ts=DateTime.Now.Ticks.ToString(),那么就达到动态地图服务每次都能看到最新结果的目的。

    关于控制子图层的可见性。REST SDK的ExportMap操作中有layers参数可以控制。比如layers=show:2,4,7,则只会显示第3、5、8图层内容。

    关于图层内容过滤。ArcGISDynamicMapServiceLayer有LayerDefinitions属性可以用SQL语句来筛选地图服务的输出内容,而REST中的ExportMap方法也提供了layerDefs供我们调用。比如{"0":"POP2000 > 1000000","5":"AREA > 100000"} ,只输出第一个图层中POP2000字段大于1000000的要素,第六个图层中AREA>100000的要素。

    关于ArcGIS Server 10中的TimeExtent。ArcGISDynamicMapServiceLayer有一个TimeExtent属性用来显示一定时间范围内的数据,而ExportMap方法也给我们提供了Time参数来实现这个功能;并且还有layerTimeOptions参数来控制每个图层的时间段(偏移)。

    关于服务的元数据。ArcGISDynamicMapServiceLayer和ArcGISTiledMapServiceLayer中,都有一些关于服务元数据的属性。比如CapabilitiesCopyrightTextDescriptionInitialExtentFullExtent等,这些在REST的MapService资源中都已经暴露了出来,因此我们可以通过发送请求的方式,在自定义图层初始化的时候顺便取回这些数据。

    关于ArcGISTiledMapServiceLayer中的TileInfo。继承TiledMapServiceLayer类的第一个条件就是得知Tiling Scheme。自然TileInfo也是探囊取物。

    细心的朋友已经看出来,其实和ArcGISDynamicMapServiceLayer、ArcGISTiledMapServiceLayer一样,我们这里自定的图层也是对ArcGIS Server REST SDK的一个封装,但在显示方式和显示速度上有所改进。输入的是动态地图服务,有缓存服务的效果,但没有真正做过缓存,所以姑且把它叫做DynamicTileMapServiceLayer。

    一贯风格,要有真相。还是来看一个实际例子感受一下吧:http://newnaw.com/pub/sl/dynamictilemapservicelayer/

    最后还要提一下这种图层的一个硬伤:面图层的重复标注。具体原因请参见:How do I avoid duplicate labels in my cache?解决办法有两种,1、采用Annotation代替Label;2、不要使用Label。

    源程序下载:http://www.arcgis.com/home/item.html?id=5e32a79350b241f38032f9ca0321ccde

ArcGIS客户端API中加载大量数据的几种解决办法(以Silverlight API为例)

    REST风格的一切事物方兴未艾,ArcGIS Server的客户端API(Javascript/Flex/Silverlight API)也逐渐站上了GIS舞台的中央。虽然客户端API给我们带来了更快捷的开发体验,更丰富的展现效果,但有些(奇怪的)需求还不能直接解决。比如要求在客户端API程序中显示大量图形(上万个),乍看之下,受到平台本身的性能制约无法完成,但我们的思维和时间一样,只要挤一挤,总还是有的。本文就来讨论一下如何在客户端API程序中显示大数据量图形的问题。以Silverlight API为例。

    要将几何对象显示在客户端程序中,一般我们首先想到的办法就是GraphicsLayer。将服务器端的要素通过查询或者其他方式创建成客户端的Graphic用以显示,从而进一步交互。于是有了下面几种方法。

1、Cluster

    比较成熟和理智的做法是,利用Cluster效果来处理大量的Graphic。对于这种办法,ArcGIS Server中的Javascript APIFlex APISilverlight API均能做到。下图是Silverlight API中300,000个点的展示效果。(忽略数据从服务器到客户端的传输时间)

image

    Google MapsBing Maps等也都采取了这种做法。

    但是,不论什么原因,就是不想采用Cluster的办法,而要看到大量密密麻麻的点时心里才觉得踏实该怎么办?对于这种需求,也有对应的解决方案。

2、直接在前台绘制Graphic

    但这种办法直接受到平台性能的限制。以Flex和Silverlight为例,在不影响地图操作的情况下,视图范围内一次性能够承受的Graphic数量大约是4000-5000个。注意,相对于绘制Graphic对象的操作来说,Geometry节点的个数可以忽略,也就是说这几千个Graphic既可以是点,也可以是面。这时候你的地图看起来是这样的(4000个Graphic):

image

3、用ElementLayer(Silverlight API独有)来模拟GraphicsLayer

     ElementLayer是Silverlight API中独有的一种图层类型,直接继承自ESRI.ArcGIS.Client.Layer,用来承载Silverlight本身的一些布局控件(Framework Element)。它的最主要的用途是使得Silverlight本身的控件能够随着地图的放大缩小而自动变化。利用它我们可以在一定程度上模拟GraphicsLayer。比如Silverlight API中的InfoWindow

    GraphicsLayer之所以只能承载一定数目的Graphic,就是因为Graphic对象本身封装的内容比较多。如果我们仅需要展示这些点,并只在上面做一些简单查询,那么可以完全用ElementLayer来替代之。直接的好处就是,可以在客户端显示更多的数据。比如一个点,我们可以用更基本的Ellipse来代替Graphic。将Graphic的Attributes存储在Ellipse.Tag中,依然可以用上面提到的InfoWindow做DataBinding。初步做了一下试验,如果用ElementLayer来模拟GraphicsLayer,在不影响地图操作的情况下,视图范围内一次性能承载的图形个数大约是20,000-25,000个。这时候你的地图看起来像是这样(20,000个Ellipse):

image

    顺便提一下,如果想在ElementLayer中来模拟动态的地图符号,甚至是时态图层也是可以的。对于Graphic,你可以定义Symbol的ControlTemplate;对于ElementLayer,则可以直接将带有任何动画的对象直接加入其中。

    上面提到的3种方法都是纯客户端的,尤其第三种办法,再没有更多工作量的情况下已经基本将客户端的性能用到了极致。但对于想看到密密麻麻点的人来说,五位数的数量级很有可能并不能满足要求。要想将展示的图形个数再提高一个或两个数量级,达到几十万甚至上百万,那么只有来借用服务器端的性能了。

4、ArcGISDynamicMapServiceLayer

    直接将所要展示的数据在服务器端发布成一个MapService,在客户端通过ArcGISDynamicMapServiceLayer来加载。这样的话客户端需要展示的仅仅是一张图片,没有任何压力。功能上,如果想查询的话可以使用Identify/Find/Query Task来达到目的。下图加载了一个本机的ArcGISDynamicMapServiceLayer(AGS10,msd发布,1个实例,core i7 2.6GHz,4g内存,win7 x64),服务只有一个点图层,记录数大约650,000条:

image

    服务器端生成这张图片的时间大约13s,进行一次IdentifyTask操作时间是0.2s。这里大部分时间都花在了服务器端对数据的绘制上,因此局域网传输图片等因素可以暂不考虑。服务器端的绘制速度受硬件和软件两个方面的影响。以ArcGIS Server 10已经无法改变,理论上只能通过提高硬件配置来达到加快出图速度的目的。

    ps:采用ArcGISDynamicMapServiceLayer时可结合服务器端的点抽希办法来一起使用(抽稀方法1抽稀方法2)。但这样就失去了密密麻麻数据带给我们的“成就感”,不在赘述。

    这种办法中,客户端的压力已经完全没有了,功能上也能通过变通来实现,但展现上有两个缺点:1、数据过多的话绘制速度太慢(40w条数据大约是6.7s,12w条数据大约是2.3s,3w条数据大约是0.6s,);2、出图过程中该服务一片空白,且移动过程中也会出现“白边”的现象。如果硬件配置也无法提高,还有没有更好的办法呢?办法是有的。

5、继承TiledMapServiceLayer来达到“动态切图”的目的

    在Google Fusion Tables的例子中,我们可以看到,所有的点数据都是以切片的方式展现,这样的好处是,即使出图速度较慢,用户也能看到切片一个一个的加载过程,不至于长时间一片空白;移动过程中也不会有白边现象;且浏览过的地方都会被客户端缓存,不需要再次向服务器请求,既减少了服务器压力,也加快了显示速度。但明显Google不可能为上传的每张表格都去做切片处理,ArcGIS Server中如何能达到这种效果?

    首先想到的是服务切图时的“Cache On Demand”设置。但第一个用户等待的时间就是服务器完成该比例尺下切图的时间,显然太长;并且切图后如果数据有变化,那么看到的切图就和实际数据不一致了。这条路走不通。

    通过继承TiledMapServiceLayer,可以利用GetUrl方法来控制如何获取每一个切片。此时如果我们以动态地图服务为资源,利用REST SDK的ExportMap操作来生成每一个切片,就达到了“动态切图”的目的。不仅如此,我们还可以在服务器端发布多个相同的服务,在GetUrl方法中轮询使用这些服务,便可以达到“并行”出图的目的。此时每个服务生成的图片都是256*256大小,所包含的记录数会少很多,速度会明显加快。本机发布3个服务时,同样比例尺下的650,000条记录,所有“切片”加载完成大约需要6s,考虑到缓存服务的加载效果,用户完全可以接受。这种方法相比ArcGISDynamicMapServiceLayer,大大提高了出图的速度和效果。

    关于此方法的几点说明:如何利用比例尺、行/列号来计算一个切片的范围,可以参考《ArcGIS Server的切图原理深入》;默认情况下,浏览过的“切片”会缓存在客户端,再次浏览时服务器端就不用动态出图了。如果不想在客户端缓存“切片”,保证服务器端数据发生变化后,客户端也能随时更新,则可以在请求后面加上时间戳,达到DisableClientCache的功能;对于这种数量级的数据展示来说,一般是局域网应用,因此Identify/Find/Query与服务器端的交互完全可以接受;而且通常拥有这种数据量的用户硬件资源一般都是可充分扩展的,因此多发布几个服务来加速出图完全可行。

    无图无真相,看看这种办法的效果。别忘了,在服务器端你发布的仅仅是普通的动态地图服务。

    好了,这几种方法各有千秋,供各位朋友按需选择。

ArcGIS API for Silverlight加载本地Shapefile文件

      可能有些朋友还不知道如何在客户端程序中显示本地的shapefile,我在这里再介绍一下。

      在线例子:http://newnaw.com/pub/sl/shapefilereader/(底图是WGS 84坐标系)

      效果:

image

image

      有些时候需要将本地的数据,比如shapefile,加载到客户端API程序中进行预览。思路不外乎一个,就是读取数据,然后将里面的要素显示在GraphicsLayer上面。但读取本地数据的方法有两种:上传到服务器端,用AO读取,然后将结果传回客户端处理;直接在客户端读取。

      由于Esri早在1998年就公开了shapefile文件格式,因此直接写底层代码读取shapefile成为了可能,比较著名的有开源的sharpmap类库等。所以理论上可以在客户端打开本地的shapefile,解析后直接将要素转换成Graphic从而显示在GraphicsLayer之上。在09年Silverlight API刚出来的时候,前辈viswaug就已经做了这个工作。开源的项目在这里:http://esrislcontrib.codeplex.com/,上面的例子就用到了其中的shapefilereader类。里面的其他功能,比如GeoRSS和HeatMap Layer都已经被收录到了最新的SL API中。

      而对于其它文件格式,比如CAD,如果有相应的类库,那么在本地直接加载也是可能的。

ArcGIS for iOS开发中Base SDK Missing的解决办法

      目前ArcGIS API for iOS的版本是1.0final,而它本身是用iOS SDK 4.0.2版本编译的,所以要求开发环境的iOS SDK是3.2或4版本以上。于是你按照要求,下载了最新的iOS SDK 4.2(November 22,2010)和ArcGIS API for iOS 1.0并安装之后,在XCode中按照ArcIGS模板新建一个简单的View-based ArcGIS Mapping Application,但这是会发现出现Base SDK Missing的编译错误。

      Google一下,这个问题很普遍,解决办法就是Project->Edit Project Settings->Build标签页中的将默认的Base SDK从3.2版本选择成现有的iOS SDK 4.2。可这样依旧不能解决问题。关键还有一个步骤,Project->Set Active Target,选择成你现在的工程。尽管你会看到,Project->Set Active Target中只有你现在工程的名称,并且已经打钩了,但你仍然要用鼠标点击一下。

      之后编译,便会在模拟器中看到HelloMap程序了。

将socks代理转为http代理

      之前使用ssh账户和myentunnel作为上国际互联网的代理,但是这个代理本身是socks类型。而ie浏览器只支持http方式的代理,所以将127.0.0.1:7070设置到ie代理后是没有作用的。

      而利用Privoxy这个工具,就可以将socks代理转成http代理。转换方法如下:

  1. 修改Privoxy的配置文件,找到listen-address这一行,将其修改为:“listen-address  127.0.0.1:6060 ”, 6060也就是你需要的http代理的端口;
  2. 再找到“forward-socks5”这一行,去掉前面的注释标记#,将这一行修改为“forward-socks5   /               127.0.0.1:7070  . ”, 7070也就是socks代理的端口;
  3. 重启Privoxy就可以了。

      最后在ie代理中填入127.0.0.1:6060,就可以访问国际互联网了。此时,也可以很方便的让别人的机器使用你的6060端口来上国际互联网~

      这样做是为什么呢?因为有些软件,比如windows live writer,只支持http类型的代理,因此Privoxy还是很给力的。对了,这篇日志就是用windows live writer写的。

ArcGIS API for Silverlight中的InfoWindow

        ArcGIS API for Silverlight自带Maptip,鼠标悬停触发;但对于Javascript API和Flex API中由单击事件触发的InfoWindow,许多朋友一直想要这种效果。
        目前2.1版的api中给出了基于Toolkit的InfoWindow例子,如果觉得它不好用或者不满意,可以参考我这个基于ElementLayer实现的InfoWindow的例子

其实我的qq一直是隐身的

        有同志说一天看不到我,其实我的qq一直是隐身的。
        一般来说,只要不出差,一周里我会5*8小时在线,我的qq一般也是5*8小时隐身。而又一般来说,我是不出差的。
        只要你不在qq上问我技术问题,我都会立刻进行回复;如果你问技术问题,我会故意拖延若干分钟至若干小时不等再给你回复,这样就显的我比较忙一些,也许你下次就不会在这么问了。因为技术问题讲起来比较麻烦,只靠打字是说不清楚的,而我又很懒,所以很讨厌在qq上回答技术问题;再说对于技术我也只是略懂皮毛,如果因为我的懒和水平不够而产生了歧义,对于问题的解答也是没有任何帮助的。
        不过如果你通过电话来问我技术问题,那你肯定能听到一个非常热情和尽可能全面的解答。
        有图有真相,看看我过去几个礼拜和接下来的几个礼拜将要做的一些事情。


        接下来的事情(其实真不喜欢这个白色的东西)。。。