ArcGIS Server 10中的切图/缓存机制深入

  两年前我写过一篇关于ArcGIS地图切图/缓存原理的文章,《ArcGIS Server的切图原理深入》,里面以tiling scheme为主,讲了缓存图片的存储结构以及相关坐标的计算。那时还是ArcGIS 9.3版本,现在ArcGIS 10已经推出快一年多了,地图缓存/切图方面有了很大改进,比如增加了compact缓存格式,增加了import/export map server cache工具,增加了mixed图片模式等。这次就在上篇文章的基础上,以ArcGIS 10为例,看看其中地图切图/缓存制作的工作机制。
  注:本文旨在深入了解ArcGIS目前切图(即制作地图服务的缓存)的机制,以帮助工作中有需要的朋友们提高工作效率,因为切图,尤其是大比例尺,耗时耗资源,实在是一件伤不起的工作。这里已经假设你知道了如何为缓存地图服务配置地图如何检查发布地图服务的地图文档的性能,开始动手前务必进行小范围、全比例尺切图试验等等注意事项,如否,请查阅相关资料。

supertile和bundle

  要深入了解ArcGIS在切图时的工作机制,有两个概念必须明白,就是supertile和bundle。
  假设一个tile(切片)的大小是256*256,在切图时如果按照这个大小直接exportmap,开启了动态标注的地图服务上线图层和面图层就会有很多重复的标注。因为exportmap时,每个tile都会包含一个要素的标注,如果一个要素横跨了多个tile(这在大比例尺下基本是肯定的),那么这些tile上就会出现相同的标注,exportmap时并不知道相邻的tile上已经有了该要素的标注。

image

  为了解决这个问题,ArcGIS在切图时引入了supertile的概念,开启抗锯齿时supertile大小为2048*2048,反之为4096*4096。真正切图时会首先exportmap出一个supertile,然后将它们分割成指定大小的tile。对于每个要素,每个supertile中只包含一次标注,这样保证一个superfile大小的范围内不会出现重复标注。事实上在绘制supertile时,会尽可能多地包含周围的标注,所以在每个supertile的边界周围仍会有重复标注。解决重复标注的根本办法就是使用annotation,而不是label。
  ArcGIS 10中推出了新的compact缓存格式,将原来离散的每个tile图片(exploded格式),保存成连续的二进制文件(.bundle),每个.bundle文件最多可存储128*128个tile。相比以前成千上万的tile文件,这样做有明显的好处:易于缓存迁移,减少占用磁盘空间,减少硬盘i/o等,esri在任何时候都推荐使用compact格式来创建缓存,除非你需要自己读取每个tile(技术上来说这点可以不成立)。ArcGIS 10在切图时,也采用了bundle的概念:对于每一级比例尺,首先从tiling scheme origin开始,将切图范围分成若干个bundle,每个bundle覆盖的范围是128*128个tile大小。比如在1:4096比例尺时,如果tile大小是256*256,DPI为96,那么每个bundle范围的大小是:((4096*2.54/96)/100/1000*256*128)^2=1261.086平方千米。之后,有且仅有(这么做是为了避免多个进程同时读写一个磁盘文件)一个服务实例/一个arcsoc.exe进程(如果是high isolated)来负责此bundle范围的切图,先输出supertile,然后再切成tile。即使选择了exploded缓存格式,ArcGIS 10中也会采用这种机制来切图。

image

  9.3.1及以前版本中,每个服务实例工作的单位是supertile。即一个arcsoc.exe负责生成一个supertile,然后切成tile,再继续生成下一个supertile……相比supertile这种处理单位来说,采用bundle作为处理单位的ArcGIS 10中的服务实例可以将更多精力投入到连续切图中去,而不是频繁切换自己负责的范围。在大比例尺切图中,这种新机制能很大程度上减少切图时间;但在小比例尺切图中,这种新机制有可能反而增加切图时间。因为小比例尺时可能全图范围只有不到一个bundle范围大小,这样只会有一个服务实例来切图,其他实例都处于空闲状态,而9.3.1及以前版本中,所有服务实例都会“蜂拥而上”。

image

网格切图和按featureclass范围切图

  有过大比例尺范围切图经验的朋友肯定会知道,一般切图策略是:小比例尺全图直接切,较小比例尺可能按照某个featureclass范围来切,而大比例尺一般是按照包含网格(grid)的featureclass范围来切。以我国全范围切图为例,小比例尺时全图切没有问题,大比例尺时如果仍然全图范围(包络矩形)切的话,会将周边其他的国家也包含进来,这并不需要的额外工作量在大比例尺时会是一场噩梦。

image

  而之所以不仅要按指定featureclass范围切图,而且featureclass里要包含网格的原因在于,便于细化和跟踪切图进度。切图工具会给你指定的featureclass创建一个新的Cached字段,将已经切好的feature标记为YES,以便在选择Recreate Empty Cache时避免重复切图,从而可以将切图工作分为多次来进行,或者以便在切图失败后排查原因,继续切图工作。
  在指定featureclass范围切图时,是顺序处理该featureclass中的所有feature的。所有的服务实例会首先集体处理一个feature范围,切出该feature范围内所有要求的比例尺级别的结果。此时ArcGIS Server会重启该服务。然后所有服务实例再去切下一个feature范围……所以与每个feature边界相交的supertile可能会被创建两次或多次(多个feature的相交处),这也是为什么在使用featureclass切图前,需要分别对它进行GeneralizeAggregateDissolve的原因。

image

  结合之前bundle的机制我们知道,如果一个feature范围恰好只包含一个bundle,那么就杯具了,因为对该feature切图时,只会有一个服务实例进行工作(一个bundle同时由且仅由一个服务实例处理),其他服务实例全部处于空闲状态。因此,比较理想的情况是,每个feature至少包含比服务实例数更多的bundle时,才能充分利用硬件资源来对其切图。
  这就会引出一个问题,就是如何来确定某个比例尺下一个bundle的大小,从而生成这样的featureclass呢?ArcGIS 10中,给我们提供了一个新的GP工具Map Server Cache Tiling Scheme To Polygons,利用它,我们可以针对某个地图服务,生成每个需要切图比例尺下所有的supertile,进而得到以每个supertile为一个feature的featureclass。以全国地图为例,在1:288,895比例尺(ArcGIS Online Tiling Scheme的第12级)下生成的supertile是这样的:

image

  我们需要的是某个比例尺下bundle的范围,如何根据supertile来确定bundle的大小呢?在N级比例尺时,一个supertile是16*16个tile,第N+1级比例尺时,该supertile会覆盖32*32个tile,第N+2级比例尺时,覆盖64*64个tile,第N+3级比例尺时,这个第N级的supertile会覆盖128*128个tile,眼熟吧,这正是一个bundle的大小。由于supertile和bundle都是从tiling scheme origin往右下角算起的,因此第N级一个supertile的范围正是第N+3级一个bundle的范围。由此如果我们要生成第15级的bundle网格,只需要用Map Server Cache Tiling Scheme To Polygons工具生成第12级supertile的网格即可,如上图。
  以此为例,我们来说明如何有效进行大比例尺切图的问题。假设我们需要对1:36,111(ArcGIS Online Tiling Scheme的第15级)这个比例尺进行切图,我们首先生成该范围的bundle网格,恰好是第12级的supertile网格,如上图。为了简便起见,我们只取其中8个feature来做说明,地图服务的最大实例数是4个(机器是单cpu,4核)。如果每个feature恰好是一个bundle,那么一个feature只能被一个arcsoc.exe处理,而其他3个服务实例均处在空闲状态(占用内存最少的那个arcsoc.exe是用来清空工作目录的,与具体服务无关):

image

  而我们对这个featureclass做一个处理,将4个feature(恰好是一个bundle)合并成一个feature(bundle cluster),这样每个feature就恰好包含了4个bundle,如此我们所开启的4个服务实例就可全速工作了,发挥了机器的最大性能:

image

  ps:Map Server Cache Tiling Scheme To Polygons工具生成的supertile是从tiling scheme origin开始计算的,而不是地图服务的fullextent左上角,因此利用它生成的supertile合并出来的bundle是最合理的,恰好与理想的bundle分界处一致。因此在大比例尺下利用featureclass切图时,应当利用Map Server Cache Tiling Scheme To Polygons来生成网格,而不是fishnet工具。在ArcGIS 10.1中,将会推出新的GP工具,会根据cpu核数来生成合理的包含bundle cluster大小feature的featureclass。
  其他方面还有一些问题,比如切图时最大服务实例数设置多少为好(一般是cpu核数+1),即使所有实例全部工作cpu占用率依然低于90%(有可能是内存不足)等问题,与切图机制无关,就不在此讨论了。
  总之,切图是一个技术活,要求还比较高,需要考虑的问题很多。如果你只把它当一项体力活来看的话,只能说明认识还不够全面,那就不能怪ArcGIS Server不好。毕竟,人家ArcGIS Online全球的19级缓存都7*24小时上线两年了,还有什么理由说产品不好呢?

佛与蜘蛛的故事(转载)

转自网络,出处不明。

  很久很久以前,在一个香火很旺的寺庙里,有一只染上了佛性的蜘蛛。有一天,佛从天上路过,看见了这个香火很旺的寺庙,佛来到了这个寺庙里,看见了那只蜘蛛,佛问:“蜘蛛,你知道什么是这个世界上最值得珍惜的吗?”
  蜘蛛回答:“得不到的和已经失去的。”
  佛说:“好,那我三千年后再来问你这个问题。”
  佛走了。
  蜘蛛仍然生活在这个寺庙,每天都在为前来许愿的人们所祈祷,每天都在为他们的故事所感动。曰子就这样在不知不觉中慢慢的过去。
  三千年后,佛又来到了这个寺庙,他又问这只蜘蛛:“蜘蛛,你知道什麽是这个世界上最值得珍惜的吗?”
  蜘蛛回答:“得不到的和已经失去的。”
  佛说:“好,那我三千年后再来问你这个问题。”
  佛走了。
  蜘蛛仍然生活在这个寺庙里。忽然有一天一阵风刮来了一滴甘露,这滴甘露就落在蜘蛛的网上,蜘蛛很喜欢这滴甘露,它每天都看着它,觉得自己很幸福,觉得时间每天过得很快。但是有一天,那阵风又刮来了,并且把甘露也带走了。蜘蛛失去了甘露,它很伤心。曰子就在蜘蛛的悲伤中慢慢的过去了。
  三千年后,佛再一次来到了这个寺庙,他又问蜘蛛:“蜘蛛,你知道什麽是这个世界上最值得珍惜的吗?”蜘蛛仍然回答:“得不到的和已经失去的。”佛说:“好,那你就和我一同到人间一趟吧。”
  蜘蛛随佛来到了人间,佛帮蜘蛛投胎转世,18年过去了。
  那只蜘蛛投胎成了一个官宦之家的小姐,取名叫珠儿。同年,甘露也投胎转世,成了金科状元。再一次皇宫的大宴上,珠儿和甘露又一次的相遇了。甘露仪表堂堂, 举止文雅,成为了众人瞩目的焦点,自然也得到了皇帝的女儿—长风公主的亲睐。珠儿并不着急,因为她知道,她和甘露的缘分是上天注定的。
  有一天,珠儿去寺庙里烧香,恰巧遇见了陪母亲来烧香的甘露。她走过去,甘露文质彬彬的说:“小姐,您有何贵干吗?”珠儿的脸色顿时变得很苍白:“你难道不 认识我了吗?我是珠儿呀,就是两千多年前那的那只蜘蛛。”甘露不解的回答:“对不起小姐,我想是你认错人了,我并不认识你,也不知道你说的到底是什麽。”
  甘露扶着母亲走了。珠儿进入了无比的悲痛之中。她不明白这份天注定的缘,怎麽这麽难。几天后,当珠儿还沉浸在痛苦中的时候她得到了两个消息:一是皇帝把自己的女儿长风公主许配给了金科状元——甘露;二是皇帝把她许配给了自己的儿子——甘草。
  听到这个消息,珠儿终于坚持不住了,她彻底的崩溃,从此一病不起。在珠儿和甘草的大婚快到的时候,得知珠儿大病不起的甘草很是伤心,他来到珠儿的床边,握着昏迷之中的珠儿的手说:“珠儿,你知道吗,自从在父皇的大宴上看见你的那一刻起,我就已经深深的爱上你了,所以我请求父皇把你许配给我,如果你死了,我这下半生……”
  珠儿已经听不见了,因为她的灵魂已经慢慢的离开了她的躯体,她看着自己身边默默流泪的甘草,感觉像有一把刀在心里狠狠的割了一下。
  正在这时,佛出现了,他问珠儿:“你现在能告诉我什麽是世界上最值得珍惜的吗?”
  珠儿含着眼泪说:“得不到的和已经失去的。”
  佛说:“难道你还不明白吗?甘露在你的生命中只是一个过客,他是被长风带来的,也是被长风带走的,所以他属于长风公主。而在你在寺庙生活的那段曰子里,在你网下的甘草,一直默默的注视着你,爱慕着你,只是他没有勇气告诉你,你也从来没有低下过你那高贵的头颅。”
  这时的珠儿早已是双眼含泪,她点点头,看着自己身边的甘草说:“在这个世界上最值得人们去珍惜的是现在身边所拥有的。”

ArcGIS API for Windows Phone开发实例(7):利用Geoprocessing分析超市的营业状况

  本文内容:Geoprocessing
  前几节中,我们利用GIS这个工具,对连锁超市店面的营业状况做了充分的展示。这一节中,我们利用GIS特有的Geoprocessing功能,结合超市的营业数据统计,对超市的营业状况做一个比较合理的判断,从而有助于经营者及时调整策略,适应市场的变化。
  我们的大致分析思路是这样的:对于每个超市来说,会有固定的消费群体,一般通常是距离该超市一定范围内的常驻居民,这部分占绝大多数(一次性路过的消费者人数较少,可以忽略不计);首先我们利用GIS这个工具,设法得到每个超市周围一定范围内的常驻人口数,做为该超市的潜在消费群体;然后根据过去一定时间内,这个品牌所有超市的营业额统计,计算出所有超市店面潜在消费群体每人每天的大约消费额度是多少,做为该品牌超市的一个平均竞争力(每人/天购买力是多少);这样就能利用这个平均竞争力数值和某个超市潜在消费人口数,预估出该超市一定时期内大约的营业额收入。如果实际营业额与这个预估相差不大,我们认为该超市营业状况正常;如果营业额统计明显小于这个预估值,则有可能是该超市营业范围内新增了竞争对手的店面,或竞争对手采取了促销措施等手段分流了购买力,从而提醒经营者及时对营业策略做出调整,避免进一步的损失。
  超市店面的营业额统计我们有了,下一步就需要获得每个超市潜在的消费人群数量。我们分两步来完成这项工作:第一步计算出距离某个超市店面一定时间车程的地理范围,作为该超市潜在消费群体的居住区域;第二部查询出该区域内的常驻人口数。
  GIS正是用于处理所有与地理分布相关任务的一个有力工具,Geoprocessing则是它的核心所在。上面的两个步骤我们就可以利用Geoprocessing轻松完成。ArcGIS Server中,将具有一定逻辑相关的一组任务封装成一个处理流程,发布成Geoprocessing Service(简称GP服务),通过Task的形式供客户端调用。客户端只需提供相关的参数后,就可直接得到处理结果,而不需要理会服务器端复杂的处理流程。ArcGIS Online为我们提供了两个现成的Geoprocessing Service:CreateDriveTimePolygonsPopulationSummary。前者用来获得距某点一定时间的车程范围,后者用来获得某范围的常驻人口,刚好符合我们的需求。因此只需调用这两个GP服务,即可得到我们想要的数据,从而完成相关计算工作。
  注:CreateDriveTimePolygons适用于美国境内,在此仅作说明之用;由于数据原因,程序内所使用的北京市内的对应GP服务无法公开。PopulationSummary适用于全世界范围,但结果精度不做保证。关于两个服务的使用限制,请查看相关页面。
  我们依然创建一个工具类,取名Analysis,该类包含两个GeoProcessing Task(Geoprocessor):_gpDriveTime和_gpPopulation,分别用于获取某超市的营业范围和该范围内的常驻人口数。

   1: private Geoprocessor _gpDriveTime = new Geoprocessor(App.Current.Resources["GPDriveTime"] as string);

   2: private Geoprocessor _gpPopulation = new Geoprocessor(App.Current.Resources["GPPopulation"] as string);

   3: Time.ExecuteCompleted += gpDriveTime_ExecuteCompleted;

   4:                 _gpDriveTime.Failed += (s, a) =>

   5:                 {

   6:                     MessageBox.Show("ServiceArea"+a.Error.Message + "n" + a.UserState);

   7:                 };

   8:                 _gpPopulation.ExecuteCompleted += gpPopulation_ExecuteCompleted;

   9:                 _gpPopulation.Failed += (s, a) =>

  10:                 {

  11:                     ChangeMapOpacity(-1);

  12:                     MessageBox.Show("Population"+a.Error.Message + "n" + a.UserState);

  13:                 };

  GP Task是ArcGIS API for Windows Phone提供给我们用来调用Geoprocessing Service的类,与Query Task,Identify Task等相同,使用分为三个步骤:准备参数,提交请求,处理结果。我们首先来看一下CreateDriveTimePolygons这个GP服务所需的参数:

image

  总共有三个参数。Direction为Input的参数,即输入参数,有两个:Input_Location和Drive_Times。因此我们只需准备好这两个参数,即可调用该服务。最后结果中,会包含Direction为Output的参数,即就是我们的结果了(esriGeometryPolygon)。

   1: void _FLayer_MouseLeftButtonDown(object sender, GraphicMouseButtonEventArgs e)

   2: {

   3:     //……

   4:     //prepare parameters for GP Service

   5:     GraphicCollection gc = new GraphicCollection();

   6:     gc.Add(e.Graphic);

   7:     CalculateServiceArea(gc);

   8:     //……

   9: }

  10: mary>

  11: /// find the service area of a given supermarket

  12: /// </summary>

  13: /// <param name="gc">the graphic collection contains the supermarket</param>

  14: private void CalculateServiceArea(GraphicCollection gc)

  15: {

  16:     ESRI.ArcGIS.Client.Projection.WebMercator wm = new ESRI.ArcGIS.Client.Projection.WebMercator();

  17:     GraphicCollection graphicCollection = new GraphicCollection();//using new gc to remain point(121000) stay on map

  18:     foreach (Graphic g in gc)

  19:     {

  20:         Graphic graphic = new Graphic()

  21:         {

  22:             Geometry=wm.ToGeographic(g.Geometry)

  23:         };

  24:         graphicCollection.Add(graphic);

  25:     };

  26:  

  27:     FeatureSet fs = new FeatureSet(graphicCollection);

  28:     List<GPParameter> parameters = new List<GPParameter>();

  29:     parameters.Add(new GPFeatureRecordSetLayer("Input_Facilities", fs));

  30:     parameters.Add(new GPString("Drive_Time_Values", "20 40"));

  31:     //execute the GP Task

  32:     _gpDriveTime.ExecuteAsync(parameters);

  33: }

  34:  

  35: riveTime_ExecuteCompleted(object sender, GPExecuteCompleteEventArgs e)

  36: {

  37:     ESRI.ArcGIS.Client.Projection.WebMercator wm = new ESRI.ArcGIS.Client.Projection.WebMercator();

  38:     foreach (GPParameter parameter in e.Results.OutParameters)

  39:     {

  40:         if (parameter is GPFeatureRecordSetLayer)

  41:         {

  42:             GPFeatureRecordSetLayer gpLayer = parameter as GPFeatureRecordSetLayer;

  43:             //40 min

  44:             gpLayer.FeatureSet.Features[0].Symbol = new SimpleFillSymbol()

  45:             {

  46:                 Fill=new SolidColorBrush(Color.FromArgb(119,153,255,153)),

  47:                 BorderBrush=new SolidColorBrush(Color.FromArgb(255,153,255,153)),

  48:                 BorderThickness=2

  49:             };

  50:             gpLayer.FeatureSet.Features[0].Geometry = wm.FromGeographic(gpLayer.FeatureSet.Features[0].Geometry);

  51:             _GLayer.Graphics.Add(gpLayer.FeatureSet.Features[0]);

  52:             //20 min

  53:             gpLayer.FeatureSet.Features[1].Symbol = new SimpleFillSymbol()

  54:             {

  55:                 Fill = new SolidColorBrush(Color.FromArgb(119, 153, 153, 255)),

  56:                 BorderBrush = new SolidColorBrush(Color.FromArgb(255, 153, 153, 255)),

  57:                 BorderThickness = 2

  58:             };

  59:             gpLayer.FeatureSet.Features[1].Geometry = wm.FromGeographic(gpLayer.FeatureSet.Features[1].Geometry);

  60:             _GLayer.Graphics.Add(gpLayer.FeatureSet.Features[1]);

  61:  

  62:             map1.ZoomTo(gpLayer.FeatureSet.Features[0].Geometry);

  63:             

  64:             //……

  65:             (_BusyIndicator.Child as TextBlock).Text = "查询服务区内人口数......";

  66:             //继续查询该范围人口数

  67:             //……

  68:         }

  69:     }

  70: }

image

  可以看到,我们在parameters集合中添加了GP服务所要求的所有输入参数,然后执行;而结果中,我们也取得了GP服务中的输出参数。这里说明一点,GP服务的参数GPParameter比较灵活,新建参数时的名称,类型一定要保持与Service Directory服务列表中一致,否则可能导致调用失败。另外由于GP服务的空间参考(4326)与我们的底图(102100)不一致,所以做了坐标转换。
  此外我们还注意到一点,CreateDriveTimePolygons服务的执行方式是“esriExecutionTypeSynchronous”即同步调用。这种方式一般用于执行过程时间较短的GP服务,缺点是客户端需要在返回结果之前等待。具体可参考这里
  获得了超市的服务范围之后,需要用第二个GP服务,PopulationSummary来获得该范围内的人口总数;之后就可按照前面的分析思路,来计算出该超市的营业额预期,与实际营业状况比较后,可得出相应的结论。

image

参考资料:
Geoprocessing概念:http://help.arcgis.com/en/arcgisdesktop/10.0/help/002s/002s00000001000000.htm

ArcGIS Server中的Geoprocessing Service:http://help.arcgis.com/en/arcgisserver/10.0/help/arcgis_server_dotnet_help/index.html#//009300000028000000.htm

ArcGIS API for Windows Phone中调用Geoprocessing Service:http://help.arcgis.com/en/arcgismobile/10.0/apis/windowsphone/help/011v/011v0000001m000000.htm

  至此,《ArcGIS API for Windows Phone开发实例》已经结束,希望大家能通过这几篇文章对ArcGIS API for Windows Phone有所了解。目前API的版本是2.2 beta,这仅仅是一个开始,随着以后大家对移动GIS的关注越来越多,相信esri也会在API中逐步推出更多的功能来满足我们更多的应用需求。
  在前不久召开的MIX11大会上,微软展示了新的Windows Phone Mongo,该版本的SDK中不仅WP的性能有了很大提升,而且有着全面支持真正的多任务,Silverlight 4,内嵌全功能IE9浏览器,支持socket通信,允许同时使用Silverlight和XNA编程模型(SL中就会有原生3D功能支持)等令人兴奋的新特性。伴随着市场占有率不断提升的这一事实,有着真正硬实力的Windows Phone有理由让我们相信不仅对于开发者,同样对于用户而言,它能给我们带来更多机会。

ArcGIS移动客户端业务数据离线方案的讨论

  我们知道对于移动GIS来说,数据主要分为两种类型:底图数据(basemap layers)和业务数据(operational layers)。在《ArcGIS移动客户端离线底图的几种解决方案》一文中,已经讨论了ArcGIS for iOS/Windows Phone/Android三种客户端底图离线的方案和解决办法,本文中则来讨论一下业务数据离线的可能性。
  首先我们得考虑一下业务数据离线的实际需求。以成熟的ArcGIS Mobile产品为例,它拥有完整的离线缓存(MobileCache)。而ArcGIS新的移动客户端中,目前均以在线服务数据为基础,没有离线数据的存储格式,因此我们首先得解决如何存储离线业务数据这个问题。我想到的格式有四种:json文本,shapefile,Sqlite,和Spatialite。因为这四种格式均已少量文件形式存在,且对空间数据有不同程度的支持,比较适合移动平台利用。对于MobileCache的使用,最常见的需求有两种,一种是数据采集(增/删/改),另一种是对离线数据的查询和分析。我们来分别考虑这两种需求。

一、数据采集

  ArcGIS for iOS/Windows Phone/Android三种移动客户端中,在线的数据采集和同步是通过FeatureLayer+FeatureService的方式来完成的。而离线的情况下,没有了FeatureService的支持,我们主要通过GraphicsLayer或FeatureLayer来完成数据的采集工作。需要考虑两个问题,一个是离线数据的获取(比如需要编辑已有图形的属性数据),另一个是离线数据的提交(将改动同步回服务器数据库中)。而在离线数据获取之后,提交之前的这段时间内,就需要以某种格式来支持数据的编辑。
  ArcGIS客户端API在2.0版本后,提供了Editor类,可以很容易完成对GraphicsLayer的图形编辑,属性编辑就不用多说了。比如ArcGIS API for Windows Phone中,我们就可以用同样的思路来完成离线编辑过程。这就是我们离线数据采集的思路。
  json文本格式。esri在REST API中定义了自己的基于json的数据存储方式,包括空间和属性数据。通过FeatureLayer加载在线服务,将其中所有的要素通过通过ToJson方法(或FeatureSet.ToJson())保存为json文本格式,就完成了数据的下载工作;离线后,可通过GraphicsLayer来加载这些json文本数据(FeatureSet.FromJson()),使用Editor类对其图形和属性进行编辑,或对新增数据进行采集。完成后,就得到了一个经过编辑的json文本;之后网络条件允许时,再次通过在线FeatureLayer的方式,将编辑后的json数据加载,利用FeatureService同步回服务器端。此方法对于iOS API和Windows Phone API可直接适用,Android API在正式版发布后应该会完善其对json格式的支持。
  shapefile格式。由于shapefile格式公开,有许多现成的类库提供了支持,因此可以考虑将它作为离线数据的载体。离线数据的获取非常方便,只需在服务器将数据导出为shapefile格式,即可分发到移动设备上。而在移动设备上加载shapefile,绘制成GraphicsLayer,每个平台都可以找找现成的类库来使用,Windows Phone见这里,iOS见这里;依然利用Editor类对GraphicsLayer的数据进行编辑和采集;最后需要将GraphicsLayer的数据写回shapefile中。之后回到室内,可将shapefile再倒入服务器数据库中。关于.NET中可供参考的几个包含Shapefile读写功能的库:sharpmapv2egisNetTopologySuite等。
  Spatialite数据库。Spatialite是基于Sqlite的一个C语言库,支持空间数据操作的单文件数据库(类似于Postgresql+PostGIS),支持WKT,WKB和自己的三种空间数据格式,此外通过PROJ.4支持动态投影,还支持Buffer,Union等空间操作,是移动平台上空间数据存储的理想选择。编辑和采集依然通过GraphicsLayer和Editor类来完成。但和上面的shapefile一样,数据的获取和提交需要做转换工作。
  Sqlite数据库。把它放在最后有两方面原因。一是在离线方案的讨论中,我们已经知道几乎每个移动平台上对Sqlite数据库都有很好的支持;二是它本身并不支持空间数据格式,所以不仅要进行格式转换,还要自己来解决空间数据的存储。但对于在不支持Spatialite的移动平台上又想使用数据库来管理数据的情况,可以考虑Sqlite。此处就不详细讨论了。
  对于数据采集这个需求,四种数据格式从理论上均可很好地支持。API中对json方式的读写有原生的支持,因此不需要进行任何格式转换;而后三种存储均需要自己来完成与Graphic的数据转换工作。总的来说,由于移动数据采集工作一般携带的业务数据量较少,因此用json文本来存储数据比较方便,也非常容易实现。

二、查询和分析

  离线的业务数据,经常需要进行一些属性查询工作,除此之外,还有空间的查询和简单的几何运算或关系判断。在ArcGIS Mobile的离线缓存中,这些功能都提供了很好的支持,但ArcGIS for iOS/Windows Phone/Android中,上述需求都是通过各种Task配合在线服务来完成的,不支持离线功能。真对上述提出的几种数据源,来分析一下查询和分析的功能如何实现。
  json文本格式。对于这种格式,在加载为GraphicsLayer后,我们可以利用Linq语言来进行类似属性查询工作;通过Envelope. Intersects方法/GraphicsLayer. FindGraphicsInHostCoordinates方法可完成空间查询(矩形)。
  Shapefile格式。同上。
  Sqlite。同上。
  Spatialite数据库。Spatialite具有原生的空间数据操作能力,包括Union,Buffer等,还可以通过sql语句进行基于空间索引的空间查询,功能比较强大,可完成离线的空间查询和分析需求。但依然需要进行ArcGIS<–>Spatialite数据格式转换。
  对于离线查询和分析这个需求,只有Spatialite数据库这种格式支持比较全面。目前Spatialite有iOS,Windows MobileAndroid等平台上的支持,Windows Phone暂时无解。  但值得注意的的是ArcGIS API for Android和ArcGIS API for iOS(1.8版本之后),都提供了GeometryEngine(ios版Android版)这个类,被称作离线的几何图形引擎。可在客户端完成GeometryService的大部分功能,比如area,length,buffer,union,cut,simplify,project等,这是具有里程碑意义的一步。很早的时候我就期待客户端的GeometryService功能了,并尝试自己实现了一小部分。我们可以同样期待在未来的Windows Phone API,甚至Javascript/Flex/Silverlight API中,给我们带来同样的惊喜。

结论:ArcGIS新的移动客户端中,离线数据采集需求可以并容易实现;简单的离线的查询和分析完全可通过json数据源实现;复杂的查询和分析实现与平台有关,iOS和Android可以实现。但还是期待ArcGIS在以后给我们带来原生的离线功能支持。
  什么?你说还有File Geodatabase API?它看上去功能是不错,但移动平台上的类库不知道esri会不会或者何时才能编译出来了。

ArcGIS API for Windows Phone开发实例(6):对超市信息进行时间查询

  本文内容:时态数据的概念,ArcGIS API中符号的运用
  本文来完成按时间对超市营业额信息查看的功能。拖动屏幕上方的滑块,当前日期会随着变化,而地图上显示的内容则是当前日期(某一天)每个店面的营业额状况:符号大小直观的表示了每个店面营业额的大体情况,当地图放大到一定程度时,显示出具体的数字,代表该店面当天营业额的多少。选中“连续查看”后,则可以以动画的形式对一段时间内的营业额进行连续播放。

clip_image002

  ArcGIS 10中强调了时态数据的概念。可以通过矢量或影像属性表中一个(某一个时刻)或两个字段(某一时间段),来表示该行数据发生或持续的时间。时态数据通常分为两类,一类是随着时间的变化,要素的图形或位置发生了变化(shape字段的变化),比如随着时间的变化,台风中心位置(点)进行了移动,或者火灾中的过火面积(面)发生了改变;另一类是随着时间的变化,要素的属性值发生了变化(非shape字段的变化),比如本例中每个超市的营业额。ArcGIS中,不同时刻或时段的数据用表中不同的行来存储,为了减少数据冗余,通常也采取多表关联的形式来管理时态数据
  本例中为了数据存储简单起见,将每天的营业额按列存储,也就是每天的营业额存储在了每个超市属性的不同字段中。这样我们就需要自己通过代码来完成时间展示的功能。当然也可以用ArcGIS提供的Transpose Fields工具,轻松将数据转换为ArcGIS能识别的分行存储的时态格式,从而利用API中Map/FeatureLayer的TimeExtent属性来完成我们的功能。
  先来看一下我们的TimeQuery工具初始化完成的工作:

   1: private bool _isActivated;

   2:         DispatcherTimer _dTimer;//use to control autoplay

   3:         private Border _Border;//stackpanel used to hold the slider and textblock

   4:         private CheckBox _chkboxAutoPlay;//auto paly animation?

   5:         private Slider _slider;//time slider

   6:         private TextBlock _textblock;//date textblock

   7:         private FeatureLayer _FLayer;//business layer

   8:         private GraphicsLayer _GLayerSymbol;//do nothing with Business FeatureLayer, using another graphicslayer to display symobls

   9:         private GraphicsLayer _GLayerText;//becuase the binding didn't work well in wp7 api, so using another textsymbol in a graphicslayer

  10:         //instead of a custom markersymbol with a text in it(using binding). 

  11:         private double _salesmin, _salesmax;//use to determine symbol size

  12:         private enum SymbolSize

  13:         {

  14:             small=20,

  15:             middle=40,

  16:             large=60

  17:         }

  18:         public Map map1 { get; set; }

  19:         public bool IsActivated

  20:         {

  21:             get { return _isActivated; }

  22:             set

  23:             {

  24:                 if (_isActivated != value)

  25:                 {

  26:                     _isActivated = value;

  27:                     if (value)

  28:                     {                        

  29:                         SlidePanel(Visibility.Visible, TimeSpan.FromMilliseconds(300));

  30:                         _FLayer.Visible = false;

  31:                         map1.Layers.Add(_GLayerSymbol);

  32:                         map1.Layers.Add(_GLayerText);

  33:                         InitSymbols();

  34:                         _chkboxAutoPlay.Visibility = Visibility.Visible;

  35:                         _slider.ValueChanged += _slider_ValueChanged;

  36:                         map1.ExtentChanged += map1_ExtentChanged;

  37:                     }

  38:                     else

  39:                     {

  40:                         SlidePanel(Visibility.Collapsed, TimeSpan.FromMilliseconds(300));

  41:                         _FLayer.Visible = true;

  42:                         map1.Layers.Remove(_GLayerText);

  43:                         map1.Layers.Remove(_GLayerSymbol);

  44:                         UnInitSymbols();

  45:                         _chkboxAutoPlay.Visibility = Visibility.Collapsed;

  46:                         map1.ExtentChanged -= map1_ExtentChanged;

  47:                         

  48:                         _slider.ValueChanged -= _slider_ValueChanged;

  49:                         _chkboxAutoPlay.IsChecked = false;

  50:                         _dTimer.Stop();

  51:                     }

  52:                 }

  53:             }

  54:         }    

  55:  

  56: /// <summary>

  57:         /// display or hide the time panel with a slide animation

  58:         /// </summary>

  59:         /// <param name="visible"></param>

  60:         /// <param name="duration"></param>

  61:         private void SlidePanel(Visibility visible, TimeSpan duration)

  62:         {

  63:             DoubleAnimation da = new DoubleAnimation();

  64:             da.Duration = duration;

  65:             if (visible == Visibility.Visible)

  66:             {

  67:                 //_Border.Visibility = Visibility.Visible;

  68:                 da.From = 0;

  69:                 da.To = 100;

  70:             }

  71:             else

  72:             {

  73:                 //_Border.Visibility = Visibility.Collapsed;

  74:                 da.From = 100;

  75:                 da.To = 0;

  76:             }

  77:             Storyboard sb = new Storyboard();

  78:             Storyboard.SetTarget(da, _Border);

  79:             Storyboard.SetTargetProperty(da, new PropertyPath("Height"));

  80:             sb.Children.Add(da);

  81:             sb.Begin();

  82:         }

  83:  

  84: private void InitSymbols()

  85:         {

  86:             foreach (Graphic g in _FLayer.Graphics)

  87:             {

  88:                 //determin symbol size

  89:                 double size = -1;

  90:                 if ((double)g.Attributes["D1101"] <= (_salesmax + _salesmin)/2 * 1 / 3)

  91:                     size = (double)SymbolSize.small;

  92:                 else if ((double)g.Attributes["D1101"] > (_salesmax + _salesmin) / 2 * 1 / 3 && (double)g.Attributes["D1101"] <= (_salesmax + _salesmin) / 2 * 2 / 3)

  93:                     size = (double)SymbolSize.middle;

  94:                 else

  95:                     size = (double)SymbolSize.large;

  96:  

  97:                 //change Businesslayer symbol to custom symbols and display in _GLayerSymbol for each store

  98:                 Graphic gsymbol = new Graphic()

  99:                 {

 100:                     Geometry = g.Geometry,

 101:                     Symbol = new PictureMarkerSymbol()

 102:                     {

 103:                         Source = new BitmapImage(new Uri("../Images/Dollar.png", UriKind.Relative)),

 104:                     }

 105:                 };

 106:                 

 107:                 (gsymbol.Symbol as PictureMarkerSymbol).Height = (gsymbol.Symbol as PictureMarkerSymbol).Width = size;

 108:                 (gsymbol.Symbol as PictureMarkerSymbol).OffsetX = (gsymbol.Symbol as PictureMarkerSymbol).OffsetY = size / 2;

 109:                 g.Attributes.Add("symbol", gsymbol);

 110:  

 111:                 //add a graphic with textsymbol, which display sales, to _GLayerText for each store

 112:                 Graphic gtext = new Graphic()

 113:                 {

 114:                     Geometry = g.Geometry,

 115:                     Symbol = new TextSymbol()

 116:                     {

 117:                         Foreground = new SolidColorBrush(Color.FromArgb(225,255,255,255)),

 118:                         Text = double.Parse(g.Attributes["D1101"].ToString()).ToString("###,###"),

 119:                         OffsetX = -size / 2+15,

 120:                         OffsetY = -size / 2+15,

 121:                         FontSize=30

 122:                     }

 123:                 };

 124:                 gtext.Symbol.ControlTemplate = (App.Current.Resources["LegendTextSymbol"] as TextSymbol).ControlTemplate;

 125:                 g.Attributes.Add("text", gtext);

 126:  

 127:                 _GLayerText.Graphics.Add(gtext);

 128:                 _GLayerSymbol.Graphics.Add(gsymbol); 

 129:             }

 130:             _GLayerText.Visible = false;

 131:         }

  在这个工具类的构造函数中,添加了一些控件,包括一个控制时间的slider,一个自动播放的checkbox,一个显示当前日期的textblock。另外找出了所有店面所有时间范围内营业额的最大值和最小值,这样有助于对我们的营业额进行“归一化”,从而决定地图符号的大小。

   1: public TimeQuery(Map map)

   2:         {

   3:             map1 = map;

   4:             _GLayerText = new GraphicsLayer();

   5:             _GLayerSymbol= new GraphicsLayer();

   6:             _FLayer = map1.Layers["BusinessLayer"] as FeatureLayer; 

   7:             _slider = new Slider()

   8:             {

   9:                 Height=84,

  10:                 Width=360,

  11:                 Minimum=1,

  12:                 Maximum=30,

  13:                 SmallChange=1,

  14:                 LargeChange=5,

  15:                 Margin=new Thickness(0,0,0,-30)

  16:             };

  17:  

  18:             _textblock = new TextBlock()

  19:             {

  20:                 Text = "时间:11月1日",

  21:                 Foreground=new SolidColorBrush(Colors.White),

  22:                 FontSize=20,

  23:                 FontWeight=FontWeights.Bold,

  24:                 Margin=new Thickness(20,0,0,0),

  25:             };

  26:             StackPanel sp = new StackPanel()

  27:             {

  28:                 HorizontalAlignment=HorizontalAlignment.Center,

  29:                 VerticalAlignment=VerticalAlignment.Center,

  30:             };

  31:             sp.Children.Add(_slider);

  32:  

  33:             //auto play box

  34:             _chkboxAutoPlay = new CheckBox()

  35:             {

  36:                 Content = "连续查看",

  37:                 FontSize = 20,

  38:                 FontWeight=FontWeights.Bold,

  39:                 Foreground = new SolidColorBrush(Colors.White),

  40:                 Visibility = Visibility.Collapsed,

  41:                 Margin = new Thickness(90, -22, 0, 0),

  42:             };

  43:  

  44:             _dTimer = new DispatcherTimer()

  45:             {

  46:                 Interval = TimeSpan.FromMilliseconds(500)

  47:             };

  48:             _dTimer.Tick += (sender, args) =>

  49:             {

  50:                 if (_slider.Value == 30)

  51:                     _slider.Value = 1;

  52:                 else

  53:                     _slider.Value += 1;

  54:             };

  55:  

  56:             _chkboxAutoPlay.Click += (s, a) =>

  57:             {

  58:                 CheckBox chkbox = s as CheckBox;

  59:                 if (chkbox.IsChecked == true)

  60:                 {

  61:                     _dTimer.Start();

  62:                 }

  63:                 else

  64:                 {

  65:                     _dTimer.Stop();

  66:                 }

  67:             };

  68:             StackPanel sp1 = new StackPanel()

  69:             {

  70:                 Orientation = Orientation.Horizontal

  71:             };

  72:             sp1.Children.Add(_textblock);

  73:             sp1.Children.Add(_chkboxAutoPlay);

  74:             sp.Children.Add(sp1);

  75:  

  76:             _Border = new Border()

  77:             {

  78:                 Background=new SolidColorBrush(Color.FromArgb(225,0,0,0)),

  79:                 Width=400,

  80:                 Height=0,

  81:                 CornerRadius= new CornerRadius(10),

  82:                 BorderBrush=new SolidColorBrush(Colors.White),

  83:                 BorderThickness=new Thickness(5),

  84:                 //Visibility=Visibility.Collapsed,

  85:                 VerticalAlignment=VerticalAlignment.Top,

  86:             };

  87:             _Border.Child = sp;

  88:  

  89:             (map1.Parent as Grid).Children.Add(_Border);

  90:  

  91:  

  92:             _salesmin = _salesmax = 0;

  93:             //find day minsales and maxsales in all graphics and all days

  94:             for (int i = 1; i <= 30; i++)

  95:             {

  96:                 string strDay = string.Empty;

  97:                 if (i < 10)

  98:                     strDay = "0" + i.ToString();

  99:                 else

 100:                     strDay = i.ToString();

 101:                 double fieldmax = (from graphic in _FLayer.Graphics

 102:                                    select graphic).Max(a => (double)a.Attributes["D11" + strDay]);

 103:                 _salesmax = _salesmax > fieldmax ? _salesmax : fieldmax;

 104:                 double fieldmin = (from graphic in _FLayer.Graphics

 105:                                    select graphic).Min(a => (double)a.Attributes["D11" + strDay]);

 106:                 _salesmin = _salesmin < fieldmin ? _salesmin : fieldmin;

 107:             }

 108:         }

  当控制时间的slider发生变化时,我们就根据当前日期的营业额,计算出每个超市店面符号的大小,从而使用新的符号来显示。

   1: void _slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)

   2:         {

   3:             int day = (int)e.NewValue;

   4:             //change text

   5:             _textblock.Text = string.Format("时º¡À间?:11月?{0}日¨?", day.ToString());

   6:  

   7:             ChangeSymbolAndText(day);

   8:         }

   9:  

  10: /// <summary>

  11:         /// change symbols and text according to current day

  12:         /// </summary>

  13:         /// <param name="day"></param>

  14:         private void ChangeSymbolAndText(int day)

  15:         {

  16:             string strDay = string.Empty;

  17:             if (day < 10)

  18:                 strDay = "D11"+"0" + day.ToString();

  19:             else

  20:                 strDay = "D11" + day.ToString();

  21:             

  22:             foreach (Graphic g in _FLayer.Graphics)

  23:             {

  24:                 //determin symbol size

  25:                 double size = -1;

  26:                 if ((double)g.Attributes[strDay] <= (_salesmax - _salesmin) * 1 / 3)

  27:                     size = (double)SymbolSize.small;

  28:                 else if ((double)g.Attributes[strDay] > (_salesmax - _salesmin) * 1 / 3 && (double)g.Attributes[strDay] <= (_salesmax - _salesmin) * 2 / 3)

  29:                     size = (double)SymbolSize.middle;

  30:                 else

  31:                     size = (double)SymbolSize.large;

  32:  

  33:                 PictureMarkerSymbol symbol = (g.Attributes["symbol"] as Graphic).Symbol as PictureMarkerSymbol;

  34:                 symbol.Height = symbol.Width = size;

  35:                 symbol.OffsetX = symbol.OffsetY = size / 2;

  36:  

  37:                 TextSymbol text = (g.Attributes["text"] as Graphic).Symbol as TextSymbol;

  38:                 text.Text = double.Parse(g.Attributes[strDay].ToString()).ToString("###,###");

  39:                 text.OffsetX = -size / 2 + 10;

  40:                 text.OffsetY = -size / 2 + 10;

  41:                 //text.FontSize = size == (double)SymbolSize.small ? size : size - 10;

  42:             }

  43:         }

  最后,还要控制营业额数字的显示范围。当比例尺较小时,超市图标分布较为密集,具体营业额数据不予显示;当比例尺较大时,才显示每个超市的具体营业额数字。

   1: void map1_ExtentChanged(object sender, ExtentEventArgs e)

   2:         {

   3:             if (map1.Resolution > 50.2185141425366)//38

   4:                 _GLayerText.Visible = false;

   5:             else

   6:                 _GLayerText.Visible = true;

   7:         }

  至此,我们的第三个功能也完成了。在这里请各位朋友思考一下,如果使用ArcGIS格式的时态数据,我们按时间查看超市营业额的功能该怎么做?相较于本文中的方法,各自的优缺点是什么?

  参考资料:
  ArcGIS中的时态数据:http://help.arcgis.com/en/arcgisdesktop/10.0/help/index.html#/What_is_temporal_data/005z00000001000000/

newnaw的一点改变

  最近http://newnaw.com上发生了两个小变化。
  首先是音乐播放器。原来的音乐播放器是这样的:
image
  现在,比较新的浏览器,比如ie9,firefox 3.6以上,chrome 9以上或其他浏览器的最新版本,音乐播放器会变成类似这样:
image  加载的时候对浏览器是否支持html5做了判断,如果支持的话就调用html5的audio标签来播放,如果不支持的话还是用原来的activeX控件来播放。这样,mac机器上也能听newnaw的音乐了~
  还有一个好的变化。如果你使用新版本的chrome或者opera浏览器的话,可能会发现,首页的加载速度比原来快了许多。理论上应该是,将近1倍~这是因为对于支持webp格式的浏览器,我将首页里所有照片的格式,从jpg换成了webp,实验证明,在肉眼无法察觉变化的情况下,webp格式的图片比jpg格式的图片至少小一半。关于webp格式,有兴趣的同学可以自己研究一下。加载webp格式图像的首页,传输数据1.02M。
image
  对于不支持webp的大多数浏览器,仍然加载jpg图片。加载jpg格式图像的首页,传输数据2.66M。
image

  鲶总说newnaw的外表看上去像上世纪末的网站。但是没有关系,因为它有一颗年轻的心~最主要的是,虽然它外表朴素,但不缺少内容:)

ArcGIS移动客户端离线底图的几种解决方案

  移动GIS中,通常将数据分为两大类:basemap layer和operational layer。前者是指漫游或导航时起参考作用的图层,这些图层内容通常不会变化,只起到视觉辅助作用,称为底图图层;后者是指存储GIS数据的图层,比如可通过这些图层来提供属性/空间查询操作,或者对其内容进行编辑,然后与服务器端进行同步,称为业务图层。
  目前ArcGIS移动产品有5种,基于Windows Mobile平台的ArcPad和ArcGIS Mobile,这两个产品已经很成熟了,都有各自的离线缓存格式,其中ArcGIS Mobile从10版本开始,可以直接读取ArcGIS Server缓存地图服务的切片文件做为basemap layer,支持exploded和compact两种格式。
  相对于以上两个老牌移动产品,三个刚出道的小弟ArcGIS for iOS,ArcGIS for Android和ArcGIS for Windows Phone就走了不同路线:依赖于ArcGIS Server的REST服务。因此几乎所有操作,包括显示地图,都需要用到ArcGIS Server发布的各种服务。这三个产品的离线功能将来肯定是会有的,但具体的时间表还无法确定。
  针对ArcGIS for iOS/Android/Windows Phone,本文提出3种可行的离线底图(basemap layer)的解决方案,供各位参考。以ArcGIS for Windows Phone为例。

1、ArcGIS Server地图服务的Exploded格式缓存文件

  ArcGIS API for Windows Phone中,提供了ArcGISTiledMapServiceLayer用来加载ArcGIS Server发布的缓存地图服务,它的原理是Map控件计算好需要加载的切片的row,col,level参数,利用ArcGISTiledMapServiceLayer里的GetTileUrl方法提供如何获得指定参数的切片文件,最后拼接成完整的地图。
  因此我们可以通过继承ArcGISTiledMapServiceLayer的父类,TiledMapServiceLayerTiledLayer,来实现自己的自定义图层,比如用它来加载Google Maps天地图等各种地图。加载这些在线地图都是通过重写GetTileUrl()方法来实现的。
  对于已经存放在硬盘上的缓存文件,该如何加载呢?这几个图层还有一个方法,GetTileSource。这个方法有一个onComplete action,可以传入ImageSource类型的参数,它比GetTileUrl来的更直接。其实GetTileSource方法中调用了GetTileUrl方法的结果(一个获得tile的url字符串),利用这个字符串向服务器端发送请求,请求回来的结果就是切片图片的二进制流,再将这个二进制流形成ImageSource,通过onComplete方法返回。
  所以我们可以抛开GetTileUrl,直接重写GetTileSource方法,来根据row,col,level参数,读取地图服务的缓存文件。首先将Exploded格式的地图服务缓存文件拷贝到手机中:

image

  包含conf.cdi(ArcGIS Server 10版本中才有,记录了缓存的全图范围)和conf.xml文件的好处是,我们可以在代码中读取这两个文件来动态生成我们的Tiling Scheme,以完成图层初始化的工作。从配置文件中读取参数后,就可以重写GetTileSource方法了。部分代码如下:

   1: protected override void GetTileSource(int level, int row, int col, Action<System.Windows.Media.ImageSource> onComplete)

   2:         {

   3:             string f = string.Empty;

   4:             if (_cacheTileFormat.ToLower().Contains("png"))

   5:                 f = ".png";

   6:             else if (_cacheTileFormat.ToLower().Contains("jpeg") || _cacheTileFormat.ToLower().Contains("jpg"))

   7:                 f = ".jpg";

   8:             else

   9:                 throw new Exception("切片格式不明:" + _cacheTileFormat);

  10:             #region Exploded读取

  11:             if (_storageFormat == StorageFormat.esriMapCacheStorageModeExploded)

  12:             {

  13:                 string baseUrl = _path;// "/WP_LocalCacheReader;component/Assets/usa_exploded/"

  14:                 baseUrl += @"/_alllayers";

  15:                 string l = "L";

  16:                 l = level.ToString().PadLeft(2, '0');

  17:                 string r = "R";

  18:                 r = String.Format("{0:X}", row).PadLeft(8, '0');

  19:                 string c = "C";

  20:                 c = String.Format("{0:X}", col).PadLeft(8, '0');

  21:                 string str = baseUrl

  22:                     + @"/L" + l

  23:                     + @"/R" + r

  24:                     + @"/C" + c + f;

  25:                 BitmapImage img = new BitmapImage(new Uri(str,UriKind.RelativeOrAbsolute))

  26:                 {

  27:                     CreateOptions = BitmapCreateOptions.DelayCreation

  28:                 };

  29:                 img.ImageFailed += (s, a) =>

  30:                 {

  31:                     string uri = _path + "/missing" + _tileRows.ToString() + f;

  32:                     BitmapImage image = new BitmapImage(new Uri(uri, UriKind.RelativeOrAbsolute))

  33:                     {

  34:                         CreateOptions = BitmapCreateOptions.DelayCreation

  35:                     };

  36:                     onComplete(image);

  37:                     return;

  38:                 };

  39:                 onComplete(img);

  40:             }

  41:             #endregion

  42:         }

  当指定的切片文件不存在(也许还未创建)时,可以加载事先准备好的missing图片来替换。

2、ArcGIS Server地图服务的Compact格式缓存文件

  这是ArcGIS Server 10推出的新的缓存格式,缓存图片都保存在.bundle文件中,一个bundle目前可存储128*128张切片。切片文件更少,主要目的是为了迁移方便。文档中并未给出读取这种格式文件的方法,不过牛魔王已经凭空推断出了这种格式的内容,这里就借鉴了他的方法。还是先将缓存文件拷贝到手机中:
image

  利用conf.cdi和conf.xml获得tiling scheme,之后重写GetTileSource方法。具体思路牛魔王文中已经给出,感兴趣的同学还是看原文,学习牛牛的思路比较好。
  下面是读取两种缓存文件的效果:
image

image

image

3、第三方离线地图文件

  除了ArcGIS Server的缓存切片之外,我们还可以读取第三方的离线地图文件来做为我们的底图。比如以前面介绍过的Mobile Atlas Creator为例,我现在已经有了很多自己下载好的离线地图,如果能在ArcGIS移动客户端使用起步两全其美?其实在目前的离线导航软件中,很多都用sqlite数据库做为地图存储格式,因为它应用广泛,轻巧,紧凑,Android,iOS,Symbian等系统对它都有原生的支持。Mobile Atlas Creator中,RMaps和OruxMaps都用Sqlite保存离线地图。这里以应用较为广泛的RMaps格式为例,进行试验。
创建好的RMaps地图文件如下:
image

  我们利用FireFox里的Sqlite Manager插件先来查看一下数据库的内容:
image

  可以看出,我们所需的内容都保存在tiles这张表中,而x,y,z三个参数与我们所需的row,col,level很像。经过试验(保存一个全球范围的地图),很快验证出level=17-z。
  参数有了,要如何读取切片呢?对于Sqlite,虽然目前Windows Phone还没有提供原生的支持,不过codeplex上已经有不少项目都提供了解决办法。我选择Sqlite Client for Windows Phone来读取RMaps的地图文件。下面是RMaps离线地图(Bing Maps)和ArcGIS Online上StreetMap叠加的效果:
image
  需要说明的是,不论是RMaps还是OruxMaps,都没有在数据库中保存tiling scheme的相关参数,所以我们不能为图层提供诸如FullExtent之类的参数。但这丝毫不影响我们的使用,我们可以为Map控件显示指定Extent,这样就可以直接显示我们的离线地图了。除了自己的地图数据之外,基本上所有数据源都使用一种空间参考,102100或者3857,你懂的。
  这样即使我们没有ArcGIS Server软件,也能制作自己的底图了。这里有网友们已经下载好的各个城市的RMaps格式文件
  相对于RMaps之类的离线地图软件,ArcGIS的移动产品的优势除了不仅能够任意叠加地图数据,还有GraphicsLayer和服务器端强大的功能支持,在配合Windows Phone本身的SDK功能,你也可以做出一个功能全面的导航软件来。

  关于离线地图文件的打包。1、在Silverlight程序中,Build Action的选择决定了文件最后的保存位置,比如你选择Resource,则会嵌入到工程的dll中,如果选择Content,则会保存在dll之外,xap文件之内。以上三种解决方案里,我们可以选择任意的Build Action,这样地图都会通过xap部署到手机里;如果有需要,我们还可以将文件拷贝到程序的IsolatedStorage中去。2、对于Exploded格式的缓存,WP的编程建议中提到,媒体文件Build Action设为Content效率会更高。3、对于Exploded格式的缓存,如果为了拷贝方便,我们也可以将其打包为.zip文件,部署到手机中,在程序加载的时候再将其解压缩来读取。
  本文以ArcGIS for Windows Phone为例,讨论了3种离线底图的解决方案,文中所涉及的所有功能,ArcGIS for iOS和ArcGIS for Android同样适用。

Mobile Atlas Creator简介

update:2011.06.08
  看起来Mobile Atlas Creator现在遇到了点麻烦。作者在sourceforge论坛上发帖说,由于收到了很多地图厂商的邮件,要求他从工具中撤下这个或那个地图数据源。。。这也是能理解的,商业公司毕竟在花大力气维护这些地图,人家不让用了我们还能怎么着?所以作者从1.9 beta2版本开始,在MAC中只保留了基于OpenStreetMap的数据源,并且取消了以前版本软件的下载。
  不过作者也善意提醒大家,如果有需要,大家可以主动尝试联系数据厂商,看看能否让自己通过MAC获取离线地图。。。当然,MAC依然继续会是一个开源的java程序,也有网友指出,既然有openstreetmap的dataprovider,从技术上讲,就可以添加别的dataprovider,大家都懂的~
=========================================================
  如果你使用过手机上的地图或导航软件,那你一定知道最头疼的问题就是如何方便地搞到离线地图。比如Google Maps,如果你没有离线地图,最高兴的就是移动运营商了,流量大把大把的。网上有不少下载Google Maps离线地图的软件,但要不然收费,要不然就是会有各种各样的问题。这里给大家介绍一个完全免费,并且功能强大的离线地图下载软件,Mobile Atlas Creator。
  Mobile Atlas Creator是一个为各种手机地图软件创建离线地图的开源应用程序。它能够用最简便的方式把常用的数据源下载成相应软件的离线地图文件,这样的话你的手机在没有联网的情况下,也能显示完整的地图了。它支持的数据源有很多,包括Google Maps, Bing Maps, Yahoo Maps, OpenStreetMap等近30种数据源。
image

  它支持的软件也很多,比如Windows Mobile系统上的OZI Explorer CE,Android平台上的OruxMaps和RMaps/BigPlanet等,这几个软件是我认为各自平台上最好用,功能最全面的离线导航软件。
image

  使用方法见下图:
MobileAtlasCreatorIntro

  下载完成后,它会生成你选择软件格式的离线地图文件,比如我这里选择RMaps,生成后缀名为.sqlitedb的地图文件。一张地图一个文件,拷贝和使用起来非常方便。
image

  值得一提的时,Mobile Atlas Creator在下载时使用了多线程(默认4个,最多15个),可以充分利用你的带宽。除了Google Map的卫星影像图片会限制IP外(如果下载速度过快,会报错),其余常用地图比如Google Maps街道图,Bing Maps的影像(完全可以用这个代替Google Maps的影像图)和街道图,都可以正常下载完成。
  根据网友建议,下载你感兴趣区域遵循以下原则即可:世界1-4级,中国4-7级,城市8-17级。当然,如果你有时间,硬盘够大,完全可以把整个Google Maps搬回家~
  去年夏天我就是用它创建的离线地图横穿了陕西,河南和河北三个省~