2011年3月29日星期二

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同样适用。

2011年3月28日星期一

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搬回家~
  去年夏天我就是用它创建的离线地图横穿了陕西,河南和河北三个省~

2011年3月27日星期日

改变

  一个人在大学的四年时间里最应该学习的是什么?18岁你满怀憧憬进学校的时候可能没有想到这个问题,20岁你在学校的时候(悠哉悠哉?刻苦学习?风花雪月?黑白颠倒?)可能没有想到这个问题,现在离开学校了,如果你还没想到或者想过这个问题,那么很遗憾,估计你现在回想起来学校生活,首先想到的可能是后悔。
  当然这个问题没有标准答案,我认为比较好的答案是首要学做人,同时学知识。就和种树种花种庄稼一样,浇水、施肥、除虫都有一个最佳时机,赶上了,会有好的收获,错过了,只能坐等后悔。第一个问题,怎样做人,做什么样的人这个我说不来也不敢说,一方面是因为每个人都有自己的标准,这个是内在的东西,形成之后难以改变;另一方面是这实在是个太私人的问题,也不应该去改变。如果能和自己面对面,你完全了解对面的你么?这是个挺难的问题,我觉得。第二个问题,学知识。凡事必有因,存在即合理。学知识是为了什么?这个问题不算隐私,可以拿出来讨论一下。去大城市赚钱?让家里人过上更好的生活?让自己活的更体面?都没错。可如果社会都没有了,外部条件都不好,生活在社会中的你怎么去赚钱?所以我更偏向下面这种答案。
  哈佛大学与Massachusetts街道相邻的地方有一个建于1901年的小门,叫做Dexter Gate,在你进入学校的时候,会看到门的正面写着:Enter to grow in wisdom,找了两张图片:

Dexter Gate1  当你离开学校的时候,会看到门的背面写着:Depart to serve better thy country and thy kind.

Dexter Gate2  也许你会说,如果我能赚很多钱,让自己和家人都过上好生活,小家好了,自然能为这个社会更好地服务。没错,是殊途同归,但这样的触发点是小的,在让大家过的更好这个目标实现之前,大环境是坏的,如果有了利益分歧,那么情况就会变得很糟;如果把出发点改成门背面这句话,过程和结果都会赏心悦目。
  这两天电视新闻关注西部地区贫困小学生的饮食问题,报道广西省都安县16000名学生从小学到初中9年时间里吃的饭菜几乎顿顿一样,就是“黄豆蒸饭”,每天晚上睡觉前自己把生米和黄豆放到饭盒里,然后放上唯一的佐料,盐,蒸熟后饭盒里的饭就是菜,菜就是饭。记着问围在墙根吃饭的学生,好吃吗?学生说,好吃。这是2011年,总理说公平正义比太阳还要有光辉。以前我有一种想法,就是去外面吃饭的时候,如果稍微剩了一点饭菜,我会安慰自己说,这不是浪费,因为已经付过钱了,农民伯伯种地的辛苦也得到了应有的回报。但现在我改变了这种想法,我可以把浪费的这一部分钱攒起来,虽然不给红十字会,但终有一天,可以以适合的途径去帮助更有需要的人。自己赚的钱给了陌生人,不觉得可惜么?左小祖咒在《感激》中唱过,多五百元你也不会富,少五百元你也不会穷,我很认同。
  说到上面哈佛的小门,我是看了昨天20110326非诚勿扰才知道的,上面有个来自美国的男嘉宾(视频27:50起),曾经在哈佛上本科,在他的心动女生已经站到他对面的时候,问最后一个问题,如果你中奖得了1000w美金,会怎么花?女生说我没什么变化,该怎么生活还怎么生活。主持人都以为男嘉宾会上去牵手离开的时候,男嘉宾却很挣扎,最后选择了放弃。原因就是上面Dexter Gate背面这句话在他心里的烙印太深了,而女嘉宾却一点也没有。
  一个学校对一个人的影响是多么的大啊,哈佛的这个小门至少从表面上影响了这个人的择偶观念,而想想我见过的大学校门呢?高大无比,宽阔无比,母校斜对面的一个学校大门修得简直和城堡一样,而这些大门有没有改变我们哪怕一点点观念?我不知道,但我知道修大门的这些钱能够改变顿顿吃黄豆蒸饭的小学生的命运,不止一个学校的所有学生。
  确实有一些东西是内在的,无法改变的,即使女生很善变。与非门的《改变》,送给女嘉宾,再合适也不过。

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

  本文内容:ArcGIS API中Task的概念,QueryTask的使用,以及Draw对象。
  空间查询GIS中一个非常常用的功能:在地图上画出任意多边形,从自己感兴趣的事物中筛选出与所画多边形有指定空间关系(通常是相交)的要素来,进一步查看。在本次开发实例中,第二个功能就是空间查询。用手势在地图上画一个范围,筛选出落入该范围的超市店面,从而进一步查看它们的营业额统计信息。
  ArcGIS API中,给我们提供了许多Task类,来完成一些常见的GIS功能,比如属性/控件查询,几何对象的拓扑处理,特定工作流的地理任务等。它们都是已经封装好的Task类,使用起来都遵循3个步骤的原则:1、为某个Task设置所需的相应参数;2、通过Task对象向服务器发送处理请求;3、接受服务器端返回的结果。所有的计算和处理工作都是由ArcGIS Server发布的REST服务来完成,是典型的客户端请求,服务器端相应的流程。
  QueryTask是ArcGIS API提供的诸多Task之一,它接受Query类型的参数。该参数有几个常用的属性,比如Where属性和Geometry属性,通过对这两个属性的设置,我们就可以完成最常见的属性查询和空间查询功能。依然将空间查询这个功能封装成一个工具,在主界面中进行调用。

clip_image002

  这里为了清晰起见,我省去与空间查询功能无关的代码(所有程序代码会在教程完结后提供下载)。要使用QueryTask的功能,我们按照前面说三个步骤来做。首先设置好查询参数Query,然后通过QueryTask对象提交查询请求:

   1: void _draw_DrawComplete(object sender, DrawEventArgs e)
   2:         {
   3:             Polygon polygon = null;
   4:             if (_usingFreeHand) //geometry is freehand polyline
   5:             {
   6:                 Polyline polyline = e.Geometry as Polyline;
   7:                 ESRI.ArcGIS.Client.Geometry.PointCollection pc = polyline.Paths[0];
   8:                 pc.Add(pc[0]);
   9:                 polygon = new Polygon()
  10:                 {
  11:                     SpatialReference = map1.SpatialReference,
  12:                 };
  13:                 polygon.Rings.Add(pc);
  14:             }
  15:             else //geometry is polygon
  16:             {
  17:                 polygon = e.Geometry as Polygon;
  18:             }
  19:  
  20:             _GLayer.Graphics.Clear();
  21:             Graphic g = new Graphic()
  22:             {
  23:                 Geometry = polygon,
  24:                 Symbol = new SimpleFillSymbol()
  25:                 {
  26:                     Fill=new SolidColorBrush(Color.FromArgb(33,255,0,0)),
  27:                     BorderBrush=new SolidColorBrush(Colors.Red),
  28:                     BorderThickness=2
  29:                 }
  30:             };
  31:             _GLayer.Graphics.Add(g); //display the geometry created by Draw object
  32:  
  33:             QueryTask queryTask = new QueryTask(App.Current.Resources["BusinessLayer"] as string);
  34:             Query query = new Query();
  35:             //102100 to 4326
  36:             ESRI.ArcGIS.Client.Projection.WebMercator wm = new ESRI.ArcGIS.Client.Projection.WebMercator();
  37:             query.Geometry = wm.ToGeographic(polygon);
  38:             query.OutFields.AddRange(new string[] { "*" });//return all attributes fields
  39:             query.SpatialRelationship = SpatialRelationship.esriSpatialRelIntersects;
  40:             queryTask.ExecuteCompleted += new System.EventHandler<QueryEventArgs>(queryTask_ExecuteCompleted);
  41:             queryTask.Failed += (s, a) =>
  42:             {
  43:                 MessageBox.Show("查询失败" + a.Error.Message);
  44:             };
  45:             _busyIndicator.Visibility = Visibility.Visible;
  46:             queryTask.ExecuteAsync(query);     
  47:         }

  代码中,我们首先利用超市图层的服务地址,初始化了一个QueryTask对象。对于Query参数,这里设置了Geometry属性,作为空间查询的图形;对OutFields参数的设置表示在查询结果中返回所有属性字段;指定空间关系为与Geometry相交。然后通过ExecuteAsync方法将查询请求提交到服务器端。注意到在设置Query的Geometry属性之前,我们对Polygon对象做了空间参考的转换,将其从102100坐标系(WGS 1984 Web Mercator Auxiliary Sphere,这是地图控件的坐标系)转换到了4326坐标系(WGS 1984,这是超市图层的坐标系),如果Query中Geometry的坐标系不正确,查询结果往往会不可预料。
  发出请求后,我们就可以在设置好的queryTask_ExecuteCompleted方法中取得查询结果了。



   1: void queryTask_ExecuteCompleted(object sender, QueryEventArgs e)
   2:         {
   3:             _busyIndicator.Visibility = Visibility.Collapsed;
   4:             if (e.FeatureSet.Features.Count == 0) //typeof(e.FeatureSet.Features)==GraphicCollection
   5:             {
   6:                 MessageBox.Show("未选中任何店面");
   7:             }
   8:             else
   9:             {
  10:                 string str = "是否查看图表详情?";
  11:                 if (MessageBox.Show(str,
  12:                 "查询结果", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
  13:                 {
  14:                     //add selected graphics to app level, so spatialquerychart.xaml could retrieve them
  15:                     App app = Application.Current as App;
  16:                     if (app.AppParameters.ContainsKey("QueryedGraphics"))
  17:                         app.AppParameters["QueryedGraphics"] = e.FeatureSet.Features;
  18:                     else
  19:                         app.AppParameters.Add("QueryedGraphics", e.FeatureSet.Features);
  20:  
  21:                     (app.AppParameters["MainPage"] as PhoneApplicationPage).NavigationService.Navigate(new Uri("/Tools/SpatialQueryChart.xaml", UriKind.Relative));
  22:                 }
  23:                 _GLayer.ClearGraphics();
  24:             }
  25:         }


  可以看到,查询结果(落入所选范围内的超市)以Graphic的形式存储在事件参数中。Graphic的Geometry就是超市的图形信息,而Attributes属性中是我们想要的所有营业信息。如果查询结果不为空,我们将其存入全局变量中,以便在另一个页面中用图表的形式来显示。这里我们使用VisiFire控件来显示图表,它是一套可用于WP上的Silverlight插件,此处不进行过多讨论,有兴趣同学可自己搜索。
  整个查询的过程在QueryTask的帮助下变得非常简单。细心的朋友可能会有疑问,我们这个空间查询的图形是如何得到的?在前面的代码中可以看到,发起查询请求的代码是写在_draw_DrawComplete这个事件中的。_draw是我们提前定义好的一个Draw对象:private Draw _draw;。
  为了方便与用户的交互,ArcGIS API中提供了Draw这个类,可以利用鼠标或手势来交互地画出Point,Polyline,Polygon,Freehand(Polyline)等几何对象,在2.2版本的API中,Draw还新增了arrow,triangle,circle,ellipses几个原生图形。

clip_image002

  使用Draw这个对象也比较简单,初始化,设定好要画的几何图形类型,然后将其IsEnabled属性设为True,就进入了交互状态,绘制完毕后,就会触发DrawComplete事件,在事件的参数中就可以得到画出的Geometry结果,得到结果之后,就可以利用Graphic将这个看不见摸不着的Geometry显示出来了,这样就达到了交互的目的。我们空间查询的几何对象就是利用Draw得到的。

clip_image004

clip_image006

参考资料:

ArcGIS API中各种Task介绍:
http://bbs.esrichina-bj.cn/ESRI/thread-45302-1-1.html

Draw对象的使用:
http://help.arcgis.com/en/arcgismobile/10.0/apis/WindowsPhone/help/011v/011v00000019000000.htm

QueryTask的使用:
http://help.arcgis.com/en/arcgismobile/10.0/apis/WindowsPhone/help/011v/011v00000016000000.htm

VisiFire:
http://www.visifire.com/

2011年3月20日星期日

ArcGIS API for Windows Phone开发实例(4):点击查看超市信息

  本文内容:Silverlight的自定义UserControl,NavigationService(页面导航),WP的Application Bar,ArcGIS API的ElementLayer。
  上一节中,已经完成了程序的准备工作,利用FeatureLayer来显示超市位置,接下来的几篇文章中我们就来依次实现程序的四个功能点:

  • 点击查看某个超市的详细信息;
  • 按空间范围查看某几个店面的销售总额;
  • 按时间的方式动态查看每个店面的营业情况;
  • 对某个店面的近期营业情况做出具体分析。

  第一个功能在GIS中叫做Identify。这个功能的作用是,当使用者激活该功能后,点击地图上的某个超市图标,会弹出一个类似气泡的小窗口(InfoWindow)显示超市名称,点击这个气泡后,程序可导航到另一个页面来显示该超市的详细信息。
  由于很多功能都是利用单击地图的方式来实现,为了不让这些功能混淆,我将程序中的四个功能分别做成不同的工具(封装成不同的类),点击某个工具后就激活它,此时地图或其中的控件就响应该工具的功能。
  我们在工程中新建一个文件夹Tools,放置与功能点有关的类;并在其中新建一个Identify.cs的文件,用来实现我们的第一个功能点。

clip_image002

   1: public class Identify
   2:     {
   3:         private bool _isActivated;
   4:         private ElementLayer _elementLayer; //InfoWindow layer 
   5:         public Map _map1 { get; set; }
   6:         public GraphicsLayer GLayer { get; set; } //supermarket layer
   7:         public bool IsActivated
   8:         {
   9:             get { return _isActivated; }
  10:             set
  11:             {
  12:                 if (_isActivated != value)
  13:                 {
  14:                     _isActivated = value;
  15:                     if (value)
  16:                     {
  17:                         if(_elementLayer ==null)
  18:                             _ elementLayer = new ElementLayer();
  19:                         if (!_map1.Layers.Contains(_elementLayer))
  20:                             _map1.Layers.Add(_elementLayer);
  21:                         GLayer.MouseLeftButtonDown += GLayer_MouseLeftButtonDown;
  22:                     }
  23:                     else
  24:                     {
  25:                         if (_map1.Layers.Contains(_elementLayer))
  26:                             _map1.Layers.Remove(_elementLayer);
  27:                         GLayer.MouseLeftButtonDown -= GLayer_MouseLeftButtonDown;
  28:                         if (GLayer.SelectedGraphics.Count() > 0)
  29:                             GLayer.SelectedGraphics.ToList()[0].UnSelect();
  30:                         _ elementLayer.Children.Clear();
  31:                     }
  32:                 }
  33:             }
  34:         }
  35:         
  36:         public Identify(Map map,GraphicsLayer glayer)
  37:         {
  38:             _map1 = map;
  39:             GLayer = glayer;
  40:         }
  41:     }

在这个类(Identify工具)的构造函数中,有两个参数:一个Map控件和一个GraphicsLayer,分别是我们程序中的Map控件和超市图层FeatureLayer(继承自GraphicsLayer),因此我们可以在Identify工具中控制它们的行为。
  IsActived属性控制着这个工具的状态,如果IsActived=ture,则该工具处于激活状态,此时地图和超市图层响应Identify工具规定的行为;如果IsActived=false,则代表程序不使用该工具,做一些清理工作。其他工具也是如此,可以用代码来控制某一时刻只能有一个工具处于激活状态。
  另外我们还可以看到,Identify这个类中还有一个ElementLayer类型的_Elayer属性。ElementLayer是ArcGIS API for Windows Phone/Silverlight/WPF中的一种图层类型,主要用来承载Silverlight中的原生对象(UIElement),比较关键的一点是,ElementLayer中的元素会随着地图范围的变化而变化(缩放/平移),而不用自己去处理这些UIElement的地理坐标。所以在这里我们就用ElementLayer来放置我们的气泡(InfoWindow)。
  来看一下Identify的主要功能:



   1: void GLayer_MouseLeftButtonDown(object sender, GraphicMouseButtonEventArgs e)
   2:         {
   3:             if (GLayer.SelectedGraphics.Count() > 0)
   4:                 GLayer.SelectedGraphics.ToList()[0].UnSelect();
   5:  
   6:             Graphic g = e.Graphic;
   7:             _ELayer.Children.Clear();//remove other infowindow
   8:             InfoWindow infoWindow = new InfoWindow(_ELayer, e.Graphic);
   9:             ESRI.ArcGIS.Client.ElementLayer.SetEnvelope(infoWindow, new Envelope((e.Graphic.Geometry as MapPoint), (e.Graphic.Geometry as MapPoint)));
  10:             _ELayer.Children.Add(infoWindow);
  11:  
  12:             e.Graphic.Select();
  13:         }


  GraphicsLayer(超市图层)的MouseButtonDown事件可以保证,只有当点击到某个Graphic(超市)后,才会触发此事件,而点击空白地方是不会触发此事件的。我们在这里并需要做任何查询操作,就可以通过被点击Graphic的Attributes属性获得该超市的所有信息,因为所有超市的属性信息(还记得OutFields属性的“*”吗)在FeatureLayer初始化的时候,已经被传送到了客户端(手机上)。点击某个超市后,屏幕上会出现一个气泡(InfoWindow类)显示超市店名。

clip_image002[5]

  这个类是我们自定义的一个UserControl,可以很方便的给其中加入额外的功能,比如点击了这个InfoWindow后,将程序导航到超市详细信息的页面。关于这个InfoWindow类详细代码以及在线例子,可以在这里查看。此外2.1以后版本的API中也提供了InfoWindow的ToolKit,有兴趣的同学可以在这里查看
  Windows Phone程序中,由于屏幕尺寸原因,如果需要另外显示比较多的内容,则需要将程序导航到一个新的页面中,比如这里我们需要显示超市的详细信息。

clip_image004

  所以在InfoWindow本身的单击事件中,用下面的代码将程序导航到显示超市详细信息的页面(Attributes.xaml)。



   1: private void Canvas_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
   2:         {
   3:             //add clicked graphic to app level, so attributes.xaml could access it
   4:             App app = Application.Current as App;
   5:             if (app.AppParameters.ContainsKey("IdentifyGraphic"))
   6:                 app.AppParameters["IdentifyGraphic"] = Graphic;
   7:             else
   8:                 app.AppParameters.Add("IdentifyGraphic", Graphic);
   9:             (app.AppParameters["MainPage"] as PhoneApplicationPage).NavigationService.Navigate(new Uri("/Tools/Attributes.xaml", UriKind.Relative));
  10:         }


  其中重要的类是NavigationService,每个Windows Phone的页面(PhoneApplicationPage)都有这样一个属性。在多页面的Silverlight程序中也使用此类来导航。虽然Navigate方法可以在页面之间传递参数(类似asp.net),但仅限于string类型,因此我们的超市属性信息集合不好处理。我在App.xaml.cs文件里的Application类中,定义一个全局的Dictionary,用于存储我们需要用到的全局变量。



   1: //used to store variables which need to pass from one page to another
   2:         public Dictionary<string, object> AppParameters;


  先将被点击的Graphic存入程序的Silverlight的全局变量AppParameters中,然后在Attributes.xaml页面就能取到。Attributes.xaml中用ListBox控件来显示超市信息,由于Windows Phone基于Silverlight 3,而Graphic的属性信息Attributes是Dictionary集合,dictionary binding在Silverlight 4中才支持,所以这里我们依然需要使用到DictionaryConverter来在绑定过程中进行转换。Attributes.xaml页面:



   1: <!--ContentPanel - place additional content here-->
   2:         <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
   3:             <ListBox x:Name="listbox" Margin="0,0,-12,0" ItemsSource="{Binding}">
   4:                 <ListBox.ItemTemplate>
   5:                     <DataTemplate>
   6:                         <StackPanel Margin="0,0,0,17" Width="432">
   7:                             <Canvas>
   8:                                 <Rectangle Fill="#FF83919D" Canvas.Left="0" Canvas.Top="0" Height="31" Width="800"/>
   9:                                 <TextBlock Text="店名"
  10:                         TextWrapping="Wrap" Margin="12,-4,4,5" FontSize="26" Foreground="White"/>
  11:                             </Canvas>
  12:                             <TextBlock Text="{Binding Path=Attributes, Converter={StaticResource MyDictionaryConverter}, ConverterParameter=Name}" 
  13:                             TextWrapping="Wrap" FontSize="30" Margin="12,28,12,3" Foreground="#FF188DC6"/>
  14:                             <Canvas>
  15:                                 <Rectangle Fill="#FF83919D" Canvas.Left="0" Canvas.Top="0" Height="31" Width="800"/>
  16:                                 <TextBlock Text="地址"
  17:                         TextWrapping="Wrap" Margin="12,-4,4,5" FontSize="26" Foreground="White"/>
  18:                             </Canvas>
  19:                             <TextBlock Text="{Binding Path=Attributes, Converter={StaticResource MyDictionaryConverter}, ConverterParameter=Address}" 
  20:                             TextWrapping="Wrap" FontSize="30" Margin="12,28,12,3" Foreground="#FF188DC6"/>
  21:                             。。。。。。。。。。。。
  22:                         </StackPanel>
  23:                     </DataTemplate>
  24:                 </ListBox.ItemTemplate>
  25:             </ListBox>
  26:         </Grid>

  Attributes.xaml.cs文件:



   1: public partial class Attributes : PhoneApplicationPage
   2:     {
   3:         private Graphic _graphic;
   4:         public Attributes()
   5:         {
   6:             InitializeComponent();
   7:  
   8:             _graphic = (Application.Current as App).AppParameters["IdentifyGraphic"] as Graphic;
   9:             GraphicCollection gc = new GraphicCollection();
  10:             gc.Add(_graphic);
  11:             listbox.ItemsSource = gc;
  12:         }
  13:     }


  完成了整个Identify功能后,在MainPage.xaml页面中,我们在Application Bar中添加四个按钮,点击某个按钮后,就激活相应的工具。比如在点击了Identify功能按钮后,我们需要将Identify工具的IsActived属性设为true,将其他工具的IsActived属性设为false。最后别忘了,在超市图层FeatureLayer初始化时(Update事件),实例化我们的四个工具:



   1: public partial class MainPage : PhoneApplicationPage
   2:     {
   3:         private Identify _OpIdentify;
   4:         private SpatialQuery _OpSpatialQuery;
   5:         private TimeQuery _OpTimeQuery;
   6:         private Analysis _OpAnalysis;
   7:         private FeatureLayer _FLayer;
   8:  
   9:         // Constructor
  10:         public MainPage()
  11:         {
  12:             InitializeComponent();
  13:             _FLayer = map1.Layers["BusinessLayer"] as FeatureLayer;
  14:             _FLayer.UpdateCompleted += (s, a) =>
  15:             {
  16:                 //Add MainPage to app level, so other pages can navigates from it.
  17:                 App app = Application.Current as App;
  18:                 if (!app.AppParameters.ContainsKey("MainPage"))
  19:                     app.AppParameters.Add("MainPage", this);             
  20:                 _OpIdentify = new Identify(map1, _FLayer);
  21:                 _OpSpatialQuery = new SpatialQuery(map1, BusyIndicator);
  22:                 _OpTimeQuery = new TimeQuery(map1);
  23:                 _OpAnalysis = new Analysis(map1,this,BusyIndicator);
  24:             };
  25:         }
  26:     }

  下一节中,我们来实现按空间范围查看某几个店面的销售总额的功能,也就是SpatialQuery这个类。

参考资料:

Windows Phone中的Application Bar和Application Menu:
http://msdn.microsoft.com/en-us/library/ff431801(v=VS.92).aspx

Windows Phone中的页面和导航框架:
http://msdn.microsoft.com/en-us/library/ff402536(v=vs.92).aspx

Silverlight中自定义UserControl:
http://www.cnblogs.com/Terrylee/archive/2008/03/08/Silverlight2-step-by-step-part10-using-user-controls.html

ArcGIS API for Windows Phone/Silverlight/WPF中的ElmentLayer:
http://help.arcgis.com/en/webapi/silverlight/samples/start.htm#ElementLayer