Tag Archives: Windows Phone

ArcGIS移动客户端中可以自动离线的底图图层

  我们都知道,在使用ArcGIS移动客户端(已更名为ArcGIS Runtime SDK for iOS/Windows Phone/Android)API进行开发时,一般对于底图数据,需要用ArcGISTiledMapServiceLayer这个类来加载,指向一个在线的缓存地图服务。而对于移动GIS应用,通常我们又有非常强烈的离线需求。目前来说,ArcGIS移动客户端底图离线有两种实现办法:自定义图层加载离线数据或等待新版本api来加载10.1的TilePackage底图离线包(iOS API已经实现)。
  这次我们来讨论另一种可以让底图离线的办法。在手机上使用过离线地图程序的朋友都知道,它们一般都会提供自动离线的功能,即当我们连接到互联网,浏览在线地图时,程序会自动将浏览过的数据缓存到本地,以便下次没有网络环境时可离线使用。新版本的Goolge Maps移动版,Android上的RMaps,OruxMaps等程序都有类似的功能。
  那么如何为ArcGIS的移动程序实现类似的功能呢?因为这个图层既要能够加载在线的地图服务,又要能够自动离线,所以我们选择创建一个派生自ArcGISTiledMapServiceLayer的类。当有网络连接时,我们浏览地图的过程中,将切片自动存储到本地,没有网络连接时就可以浏览这些切片了。而且可以给用户提供若干选项,比如是否允许自动下载缓存切片,是否允许自动更新已有的缓存切片等。
  请注意本文讨论内容与之前讨论的自定义图层加载离线数据不同,前者具有自动下载缓存切片的功能,下载后没有网络连接的情况下会自动变成离线图层加载离线数据,有网络连接的情况下可当做正常的在线图层使用;后者只能当做离线图层使用,不能自动下载缓存切片。
  这里我以Windows Phone为例,写好了一个扩展类:OfflineableTiledMapServiceLayer(下载地址见最后)。下面对它进行一些说明。
功能:
  OfflineableTiledMapServiceLayer是一个继承自ArcGISTiledMapServiceLayer的自定义类,在线浏览地图的过程中,会自动将缓存切片保存在Sqlite数据库文件中(存储在应用程序的IsolatedStorage空间内),无需人工干预;这样在没有网络连接的情况下,就自动加载先前保存过的切片,作为离线图层使用(程序代码无需做任何修改)。

如何使用:
  与ArcGISTiledMapServiceLayer用法一致,只需额外设置一些参数即可。
<esri:Map x:Name=”map1″>
    <my:OfflineableTiledMapServiceLayer Url=”http://services.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer” EnableOffline=”True”  SaveOfflineTiles=”True” SaveTilesMode=”SaveOnly”
DeleteSavedOfflineTiles=”False” LoadOfflineTileFirst=”True” />
</esri:Map>

工作流程:
  除了ArcGISTiledMapServiceLayer已有的功能外,OfflineableTiledMapServiceLayer可在浏览地图的过程中,自动将缓存切片保存到Sqlite数据库中。当下次加载该服务时(以URL地址识别),就可从离线的数据库中加载切片数据(没有网络连接的情况下)。
  OfflineableTiledMapServiceLayer目前有以下5个属性可设置:

  • EnableOffline: 默认为True。当设置为True时,OfflineableTiledMapServiceLayer具有自动离线的能力;当设置成False时,OfflineableTiledMapServiceLayer完全和ArcGISTiledMapServiceLayer一样。
  • SaveOfflineTiles: 默认为True。当设置为True时,OfflineableTiledMapServiceLayer会将浏览过的缓存切片保存到本地的Sqlite数据库中(存储在应用程序的IsolatedStorage空间);当设置为False时,OfflineableTiledMapServiceLayer不会保存任何缓存切片。只有当LoadOfflineTileFirst==false时才会生效。
  • LoadOfflineTileFirst: 默认为False。当设置为True时,OfflineableTiledMapServiceLayer会优先从本地Sqlite数据库中加载缓存切片(即使有Internet连接),如果处于离线状态,则会直接从本地Sqlite数据库中加载缓存切片(如果之前没有保存过任何切片,则会抛出异常);当设置为False时,则会优先从在线服务中加载缓存切片,如果处于离线状态,则只能从本地Sqlite中加载数据。
  • SaveTilesMode: 默认为’SaveOnly’。当设置为’SaveOnly’时,OfflineableTiledMapServiceLayer只会存储新的缓存切片(Sqlite数据库中没有的);当设置为’SaveOrUpdate’时,OfflineableTiledMapServiceLayer会存储新的缓存切片,并且更新已有的缓存切片。当在线的缓存地图服务内容更新时,此选项比较有用。只有当LoadOfflineTileFirst==false && SaveOfflineTiles==true时有效。
  • DeleteSavedOfflineTiles: 默认为False。当设置成True时,会首先删除Sqlite数据库中已有的本图层数据,之后重新初始化本图层。只有当EnableOffline==True时有效。

Sqlite文件结构:
  Sqlite在移动设备上具有广泛的应用基础,iOS和Android对其均提供原生支持。OfflineableTiledMapServiceLayer使用Sqlite Client for Windows Phone来读写Sqlite数据库。可以使用Windows Phone 7 Isolated Storage Explorer或者Isolated Storage Explorer Tool将Sqlite文件从Windows Phone的IsolatedStorage中导出,以供其它移动程序使用,比如ArcGIS Runtime SDK for iOS/Android。
  OfflineableTiledMapServiceLayer创建的Sqlite数据库名称为”OfflineTiles.db”,它的内容由一张或多张表组成。

  • ‘MapServices’表:此表是OfflineableTiledMapServiceLayer必须使用的。具有四个字段:’url'(text), ‘spatialreference'(text), ‘fullextent'(text), ’tileinfo'(text)。存储在Sqlite中不同的OfflineableTiledMapServiceLayer由其Url属性区分(假设不同的Url代表不同的缓存地图服务)。另外三个字段分别以JSON格式存储了缓存服务对应的信息,这些信息是在离线状态下初始化图层所需要的。创建该表的SQL语句: CREATE TABLE “MapServices” (“url” TEXT PRIMARY KEY  NOT NULL  UNIQUE , “spatialreference” TEXT NOT NULL , “fullextent” TEXT NOT NULL , “tileinfo” TEXT NOT NULL )
  • 其它表:如果Sqlite中存储过任何OfflineableTiledMapServiceLayer缓存图片,则除了上述MapServices表外还会有其它表。其余每张表以该缓存地图服务的Url字符串命名。这些表包含有4个字段:’level'(integer), ‘row'(integer), ‘column'(integer), ’tile'(blob)。一目了然。创建该表的SQL语句:CREATE TABLE “HereIsYourServiceURL” (“level” INTEGER NOT NULL , “row” INTEGER NOT NULL , “column” INTEGER NOT NULL , “tile” BLOB NOT NULL ), 创建索引的SQL语句: CREATE UNIQUE INDEX ‘idx_ HereIsYourServiceURL ‘ ON ‘ HereIsYourServiceURL ‘ (‘level’ ASC, ‘row’ ASC, ‘column’ ASC)

OfflineableTiledMapServiceLayer的下载地址(包括源码,示例程序,示例Sqlite离线文件):http://www.arcgis.com/home/item.html?id=d2b40d7f553947a2b575556b057f5dcf

Windows Phone 7 解锁/破解/越狱步骤 NODO版本有效

  为什么要解锁windows phone机器?不解锁只能通过zune marketplace来下载和安装市场上的软件,解锁后才能用windows phone developer tools里的application deployment工具将自己的.xap程序部署到手机上。当然只是临时做测试用,有条件还是需要注册微软每年99美元的开发者账号,来绑定自己的机器以部署程序吧,大家懂的。
  现在国内市面上还没有windows phone行货,有消息称最早在今年8月份行货机器有望上市。目前windows phone有4个版本,去年发布时最初的第一个版本7.0.7004.0,今年2月份的第一个更新版本7.0.7008.0,4月份的NODO版本(7.0.7390.0),和最新的7.0.7392.0安全更新版本。这两天拿到了htc的hd7机器做测试,是未解锁的NODO版本,无奈中文资料比较少,只好上神坛xda找答案。
  目前wp的解锁工具只有一个,就是http://www.chevronwp7.com/放出的ChevronWP7,但是该工具只适用于7004和7008版本,7390以上版本无效,必须刷回之前的版本才能解锁。解锁之后需要额外的步骤,保证zune同步时机器不会重新被锁,并且升级到NODO版本后,解锁依然有效。现将完整步骤整理如下(主要参考http://forum.xda-developers.com/showthread.php?t=1043000此贴中TechJunkiesCA的回复)。

一、将7390版本刷回最初的7004版本(如果你是7004或7008版本,可跳过此步骤)

  按照http://forum.xda-developers.com/showthread.php?t=876451这个帖子中的说明,首先确定手机的原始运营商:

In order to identify your HD 7 phone ROM version see in your phone settings>about>more information:
Firmware revision number: 2250.09.15401.728
Your phone ROM version will be: 1.54.728.01
The characterisric value (CV) is: 728
Your Operator name and CIDs i.e.: hTC_Asia_SEA operator, with CIDs HTC__044, HTC_621, HTC__622, HTC__038 can be found with this CV from following classification:
1. 666 : BellMobilty operator, with CID BM___001
2. 707: hTC_Asia operator, with CIDs HTC__044, HTC_621, HTC__622, HTC__038
3. 728: hTC_Asia_SEA operator, with CIDs HTC__044, HTC_621, HTC__622, HTC__038
4. 401: HTC_Europe operator, with CIDs HTC__001, HTC__203, HTC__102, HTC__032, HTC__405, HTC__304
5. 207: O2_DE, with CID O2___102
6. 206: O2_UK, with CID O2___001
7. 841: Telstra operator, with CID TELST001
8. 901: TIM operator, with CID TIM__401
9. 531: TMOUS operator, with CID T-MOB010

  这台机器Firmware最后三位数字是207(对应O2_DE),可得知是销往德国(de)的o2机器。找到帖子下面贴出的对应rom,RUU_Schubert_O2_DE_1.61.207.01_Radio_5.52.09.16_22 .33a.50.10U_by_ansar,下载,解压。
1、长按电源键,关机
2、按住下音量键不放,按电源键,进入bootloader(三色屏)模式(此时可松开下音量键)
3、用usb线将手机与电脑连接
4、等待windows系统自动查找并安装驱动程序
5、驱动安装完成之后,确保手机屏幕上白色屏部分显示“usb”字样(驱动安装之前是“serial”)
image
6、此时可以运行下载好的ROMUpdateUtility.exe(ruu工具),严格按照屏幕提示操作
  如果你之前用过HTC的RUU刷windows mobile机器或者刷android机器,那么最后一步应该非常熟悉。刷机完成之后,检查OS Version应当是7004或者7008。

二、利用ChevronWP7对手机进行解锁

  首先到http://forum.xda-developers.com/showthread.php?t=938106这个帖子中下载必要的工具:Windows Phone 7 Developer ToolsChevronWP7工具TCPView for Windows。WPDT对于开发者来说不是问题,肯定已经安装了(否则你需要先安装vs2010,然后安装WPDT)。下载ChevronWP7工具,压缩包中会包含ChevronWP7.exe,ChevronWP7.cer和ChevronWP7.reg三个文件。之后可以开始:
1、在电脑上运行ChevronWP7.reg。会将注册表中[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsPhoneProxyPorts]的DeviceReg一项值改为dword:000069c5
2、在手机上安装ChevronWP7.cer证书。可将ChevronWP7.cer做为附件发送到自己的邮箱,然后在手机上用浏览器打开该附件,屏幕会显示黑底的小白盾,继续点击“Install”即可
3、确保你机器的443端口没有被占用。运行tcpview.exe工具,查看Local Port一列中显示为https的程序(会占用443端口),全部结束之
4、将手机设置为飞行模式
5、将手机利用usb线与电脑连接,使用Zune软件进行非guest方式同步。如果没有安装Zune(windows自己的itunes),需要提前安装。如果同步方式那块显示的是“Guest Sync”,点击之,运行一下向导,使得你的计算机变成该手机的主要同步电脑(非guest方式同步)
6、以管理员身份运行ChevronWP7.exe进行解锁。确保手机屏幕没有锁定(可在设置中将桌面锁定设为never),处于开始画面;以管理员身份运行ChevronWP7.exe,将两个方框打钩,点击unlock。顺利的话不到5秒钟即可完成,按钮会变成relock。此时最好不要拔出usb线,接着进行下面的第三步。
  至此已完成解锁。可利用WPDT自带的Application Deployment工具,在手机屏幕处于非锁定的状态下,部署任意的.xap文件了。
image

三、确保下次用Zune同步时,手机不会重新被锁

  仅仅利用ChevronWP7.exe解锁后,如果断开与usb线的连接,以后再次插入usb,用Zune同步后,机器可能再次被锁,还需要进行解锁步骤。为防止此情况发生,需要进行以下步骤,确保以后用Zune同步时,机器不会被再次锁定。下面的步骤只适用于htc机器(因为touchxplorer只能在htc机器上运行),其他手机请参考这里
1、按照http://forum.xda-developers.com/showthread.php?t=913748此贴中的说明,依次下载TouchXplorerHTC Connection Setupunlockit(其实是XBMOD.Files.Deployer)三个.xap文件,分别解压并且将它们安装到手机中
2、在手机上运行XBMOD.Files.Deployer。会将CustClear.provxml和restoreCustClear.provxml两个文件解压到My Documents/My Ringtones目录下
3、运行TouchXplorer工具(wp里的资源管理器),将上述两个文件拷贝到Windows目录下
image
4、运行HTC Connection Setup工具,点击ok,此步骤可选。如果你的手机里有sim卡,需要执行此步,否则可跳过
  至此已经完成了所有的步骤。可以利用Zune同步后将你的手机升级至最新的7392 NODO版本,也不会被再次锁定了:)
image

如何部署大于64M的.xap文件到手机上?

  下载了大名鼎鼎的The Harvest(xap大小86.4m)想尝试一下,但用Application Deployment部署时会提示“Error-设备没有连接”。搜索xda,发现有人说在更新NODO后,大于75M的xap便无法部署了,大家纷纷跟帖表示确有此问题。但万能的xda网友toothfish再次给出解决办法
1、备份你的xap,以防万一
2、用解压缩工具打开xap文件,将其中体积较大的一些文件,比如Content文件夹解压缩出来,并从压缩包里删除之
3、记下WMAppManifest.xml文件中的productID的值(一个guid)
4、将修改过后的xap(体积应该小于64m)部署到机器上
5、使用Windows Phone Device Manager自带的Touchexperience工具,将第二步中分离出来的那部分文件,拷贝到手机的ApplicationsInstall”Product ID”Install目录中,注意维持与原xap文件中同样的目录结构
6、你已经将大xap文件部署成功了~
  其实原理比较简单,做过windows phone开发的朋友都知道,一般会将程序中用到的资源文件,比如一些媒体文件,以Content方式编译到xap中单独的文件夹里;而windows phone程序部署的真正过程也如上所示,只是将xap解压到了特定的文件夹中。因此便可以用上面的办法来移花接木了。
  还要提醒一点,我的机器上已经安装了mango sdk,所以会和Windows Phone Device Manager程序有冲突,因此我用Advanced Explorer来完成拷贝文件到手机中的过程。  image

  无法部署大xap的情况好像只会在机器上既装了mango sdk,并且手机版本为NODO时才会出现,否则(比如将手机还原到7008版本)应当可以直接部署成功。

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 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/

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

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/

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

ArcGIS API for Windows Phone开发实例(3):在地图上显示超市数据

  本文内容:GraphicsLayer,FeatureLayer,自定义地图符号。
  我们的应用场景是对超市数据进行展示,查询和分析,因此第一步当然是要将超市显示在地图之上。如所有的GIS软件一样,要将数据加载到地图里面,需要图层的概念,这里我们也需要合适的图层,将超市数据加载进来。
  先看一下ArcGIS API for Windows Phone/Silverlight/WPF中的主要图层类型。

Layer
  |–TiledMapServiceLayer
  |       |–ArcGISTiledMapServiceLayer
  |–DynamicLayer
  |       |–DynamicMapServiceLayer
  |                 |–ArcGISDynamicMapServiceLayer
  |                 |–ArcGISImageServiceLayer
  |                 |–GPResultImageLayer
  |–GraphicsLayer
  |       |–FeatureLayer
  |–ElementLayer

  Layer最基本的基类,常用的几种图层类型我已经用红色标记出来了。前两种ArcGISTiledMapServiceLayer和ArcGISDynamicMapServiceLayer分别用于加载ArcGIS Server发布的缓存地图服务和动态地图服务,这个在上一节中已经说过。再来看一下中间的GraphicsLayer和FeatureLayer,可以说这是API中最常用的两种图层,大部分的程序功能都要基于它们来完成。
  GraphicsLayer是保留在内存中的一种图层(与ArcMap、ArcGIS Engine、ArcGIS Server ADF程序中的相应概念类似),顾名思义是很多Graphic的集合,而所有与用户交互的内容通常都用Graphic来显示。比如多边形查询中用户画出的多边形,属性/空间查询结果中的所有要素等内容,都是Graphic。可以说,除了地图本身,基本上看到的所有与地理位置有关的东西都可以用Graphic来表示。Graphic对象有3个重要的属性:Geometry,Symbol和Attributes。Geometry代表了一个Graphic的几何形状(可疑是点、线、面任意一种)或地理位置,而Symbol则表示Graphic的呈现样子,比如颜色、效果,同时有了这两个属性(缺一不可),Graphic就可以显示到地图上了。而Attributes是键值对集合,可在里面存储任意类型的对象,比如一个要素的属性信息。
  FeatureLayer继承自GraphicsLayer,它与后者的区别是,GraphicsLayer中的Graphic都是人为创建出来的,而FeatureLayer中的Graphic都是从ArcGIS Server发布的服务中读取出来的,因此FeatureLayer比GraphicsLayer多了一个URL属性。这个URL通常指向一个ArcGIS Server发布的,MapService或FeatureService的子图层(对应一个FeatureClass)。FeatureLayer有了这个URL后,就可以读取出该服务对应子图层里的所有要素内容,因此FeatureLayer里Graphic的Geometry属性会自动被FeatureClass的Shape字段填充,而Graphic的Attributes字段则会根据要求,被FeatureClass中的属性信息所填充。如果发布服务的服务器是ArcGIS Server 10版本,则Graphic的Symbol属性会自动被服务的DrawingInfo信息填充。另外,FeatureLayer是客户端API中对FeatureService的唯一载体,这是它另一个非常重要的作用(也是主要作用),以后的文章中会提到。
  ArcGIS API for Windows Phone/Silverlight/WPF/Javascript/Flex/iOS/Android都是基于ArcGIS Server所发布的REST服务,所以,在开始之前,我们需要将超市数据用ArcGIS Server发布成MapService。这个过程不属于本文要讨论的内容,不熟悉的同学可自己查阅相关资料。超市图层的属性表如下,前面是每个店面的基本信息,后面是该月每天的营业额信息:

image

  超市的ShapeFile在这,点我下载。在我本机上发布出来的地址是:http://localhost/ArcGIS/rest/services/supermarket/MapServer/0
  现在打开上一节我们完成的HelloMap程序,来继续改造之。底图服务我们依旧使用ArcGIS Online提供的StreetMap数据http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer,然后我们在Map控件中加入一个FeatureLayer,其URL属性指向发布在本机的超市地图服务。由于我们的数据范围仅限于北京市附近,因此我们可以利用Map的Extent属性指定地图的初始范围,代码现在看起来是这样的:

   1: <esri:Map x:Name="map1" Extent="12926244,4840437,12982108,4878585">

   2:             <esri:Map.Layers>                

   3:                 <esri:ArcGISTiledMapServiceLayer ID="BaseMap" Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer" />

   4:                 <esri:FeatureLayer ID="BusinessLayer" 

   5:                     Url="http://localhost/arcgis/rest/services/supermarket/MapServer/0"

   6:                     OutFields="*" />

   7:             </esri:Map.Layers>

   8:         </esri:Map>

  其中FeatureLayer的OutFields属性代表从服务器端返回的属性字段,这里“*”表示返回所有属性字段,也就是我们的Graphic对象的Attributes里自动填充了超市的所有属性值。
  需要注意,在xaml代码中,最靠上的图层在显示时会出现在Map控件的最底端,如图:

clip_image002[9]

  Silverlight中,可以将常用的任何资源保存到Resource集合中,以便重复利用。因此我们将两个服务地址保存到App.xaml文件的ResourceDictionary中(可在所有页面中直接引用):

   1: <Application 

   2:     x:Class="esridemo.App"

   3:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"       

   4:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   5:     xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"

   6:     xmlns:esri="clr-namespace:ESRI.ArcGIS.Client;assembly=ESRI.ArcGIS.Client"

   7:     xmlns:sys="clr-namespace:System;assembly=mscorlib"

   8:     xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone">

   9:  

  10:     <!--Application Resources-->

  11:     <Application.Resources>

  12:         <sys:String x:Key="BaseMap">http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer</sys:String>

  13:         <sys:String x:Key="BusinessLayer">http://localhost/arcgis/rest/services/supermarket/MapServer/0</sys:String>

  14:     </Application.Resources>

  15:  

  16:     <Application.ApplicationLifetimeObjects>

  17:         <!--Required object that handles lifetime events for the application-->

  18:         <shell:PhoneApplicationService 

  19:             Launching="Application_Launching" Closing="Application_Closing" 

  20:             Activated="Application_Activated" Deactivated="Application_Deactivated"/>

  21:     </Application.ApplicationLifetimeObjects>

  22:  

  23: </Application>

  将MainPage.xaml中的代码改写成:

   1: <esri:Map x:Name="map1" Extent="12926244,4840437,12982108,4878585">

   2:             <esri:Map.Layers>

   3:                 <esri:ArcGISTiledMapServiceLayer ID="BaseMap" Url="{StaticResource BaseMap}" />

   4:                 <esri:FeatureLayer ID="BusinessLayer" 

   5:                     Url="{StaticResource BusinessLayer}"

   6:                     OutFields="*" />

   7:             </esri:Map.Layers>

   8:         </esri:Map>

  运行一下

image
 
  可以看到,超市已经显示在地图上面了。但超市的符号依然是ArcMap配置地图文档时的默认值,这里我们来利用FeatureLayer的FeatureSymbol属性,改变一下超市的地图符号。
在App.xaml文件的资源中,添加一个MarkerSymbol类型的超市符号:

   1: <esriSymbols:MarkerSymbol x:Key="SuperMarket">

   2:             <esriSymbols:MarkerSymbol.ControlTemplate>

   3:                 <ControlTemplate>

   4:                     <Canvas>

   5:                         <VisualStateManager.VisualStateGroups>

   6:                             <VisualStateGroup x:Name="SelectionStates">

   7:                                 <VisualState x:Name="Selected">

   8:                                     <Storyboard>

   9:                                         <Storyboard.Children>

  10:                                             <Storyboard>

  11:                                                 <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ellipse" Storyboard.TargetProperty="(UIElement.Visibility)">

  12:                                                     <DiscreteObjectKeyFrame KeyTime="00:00:00">

  13:                                                         <DiscreteObjectKeyFrame.Value>

  14:                                                             <Visibility>Collapsed</Visibility>

  15:                                                         </DiscreteObjectKeyFrame.Value>

  16:                                                     </DiscreteObjectKeyFrame>

  17:                                                     <DiscreteObjectKeyFrame KeyTime="00:00:00.01">

  18:                                                         <DiscreteObjectKeyFrame.Value>

  19:                                                             <Visibility>Visible</Visibility>

  20:                                                         </DiscreteObjectKeyFrame.Value>

  21:                                                     </DiscreteObjectKeyFrame>

  22:                                                 </ObjectAnimationUsingKeyFrames>

  23:                                             </Storyboard>

  24:                                             <Storyboard RepeatBehavior="ForEver">

  25:                                                 <DoubleAnimation BeginTime="0" Storyboard.TargetName="ellipse" Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)" From="1" To="10" Duration="00:00:01" />

  26:                                                 <DoubleAnimation BeginTime="0" Storyboard.TargetName="ellipse" Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)" From="1" To="10" Duration="00:00:01" />

  27:                                                 <DoubleAnimation BeginTime="0" Storyboard.TargetName="ellipse" Storyboard.TargetProperty="(UIElement.Opacity)" From="1" To="0" Duration="00:00:01" />

  28:                                             </Storyboard>

  29:                                         </Storyboard.Children>

  30:                                     </Storyboard>

  31:                                 </VisualState>

  32:                                 <VisualState x:Name="Unselected" />

  33:                             </VisualStateGroup>

  34:                         </VisualStateManager.VisualStateGroups>

  35:                         <Ellipse Height="20" Width="20" Canvas.Left="-10" Canvas.Top="-10" RenderTransformOrigin="0.5,0.5" x:Name="ellipse" IsHitTestVisible="False" Visibility="Collapsed">

  36:                             <Ellipse.RenderTransform>

  37:                                 <ScaleTransform />

  38:                             </Ellipse.RenderTransform>

  39:                             <Ellipse.Fill>

  40:                                 <RadialGradientBrush>

  41:                                     <GradientStop Color="#00FF0000" />

  42:                                     <GradientStop Color="#FFFF0000" Offset="0.25" />

  43:                                     <GradientStop Color="#00FF0000" Offset="0.5" />

  44:                                     <GradientStop Color="#FFFF0000" Offset="0.75" />

  45:                                     <GradientStop Color="#00FF0000" Offset="1" />

  46:                                 </RadialGradientBrush>

  47:                             </Ellipse.Fill>

  48:                         </Ellipse>

  49:                         <Image Height="60" Width="60" Canvas.Left="-30" Canvas.Top="-30" Source="Images/SuperMarket.png" x:Name="ellipse1" />

  50:                     </Canvas>

  51:                 </ControlTemplate>

  52:             </esriSymbols:MarkerSymbol.ControlTemplate>

  53:         </esriSymbols:MarkerSymbol>

clip_image002[11]

  好了,已经完成了我们的准备工作。下节中,我们来完成点击查询(Identity)的功能。
  相关链接:

超市信息的shapefile下载:
http://blog.newnaw.com/wp-content/uploads/2011/03/supermarket_shp.zip

Map控件中添加图层:
http://help.arcgis.com/en/webapi/silverlight/help/0166/016600000019000000.htm

GraphicsLayer概述:
http://help.arcgis.com/en/webapi/silverlight/help/0166/01660000001q000000.htm

GraphicsLayer详述:
http://bbs.esrichina-bj.cn/ESRI/thread-44892-1-1.html

FeatureLayer概述:
http://help.arcgis.com/en/webapi/silverlight/help/0166/016600000015000000.htm

图层类型小结:
http://bbs.esrichina-bj.cn/ESRI/thread-45537-1-1.html

ArcGIS API for Windows Phone/Silverlight/WPF中自定义符号:
http://help.arcgis.com/en/webapi/silverlight/samples/SymbolGalleryWeb/start.htm

Silverlight中的StaticResource:
http://msdn.microsoft.com/en-us/library/cc189045(v=vs.95).aspx