Tag Archives: Mobile GIS

在新的Android工程中调用已经编译好的so库文件代码

  依旧是Android,NDK,JNI相关的问题。
  照着ndk帮助的说明,运行<ndk>/samples目录下的hello-jni工程后,你一定想知道如何在自己的项目里调用别人已经编译好的.so库文件。于是新建一个Android工程,将hello-jni例子中ndk-build后的结果:libs文件夹(包括其下所有内容)拷贝到新建工程的根目录下,仿照hello-jni的代码,调用native方法:

   1: package com.my.SoTest;

   2:  

   3: import android.app.Activity;

   4: import android.os.Bundle;

   5: import android.widget.TextView;

   6:  

   7: public class SoTestActivity extends Activity {

   8:     /** Called when the activity is first created. */

   9:     @Override

  10:     public void onCreate(Bundle savedInstanceState) {

  11:         super.onCreate(savedInstanceState);

  12:         

  13:         TextView tv=new TextView(this);

  14:         tv.setText(stringFromJNI());

  15:         setContentView(tv);

  16:     }

  17:     public native String stringFromJNI();

  18:     static {

  19:         System.loadLibrary("hello-jni");

  20:     }

  21: }

  运行后,logcat记录以下错误:

No implementation found for native Lcom/my/SoTest/SoTestActivity;.stringFromJNI ()Ljava/lang/String;

java.lang.UnsatisfiedLinkError: stringFromJNI

  在hello-jni的代码注释里明确说明,UnsatisfiedLinkError错误是由于native代码中没有该方法的实现而引起的,但stringFromJNI这个方法,在c代码中是有的;前一个错误就比较明确了,是在Lcom/my/SoTest/SoTestActivity中去找这个方法实现的,打开hello-jni.c文件看看该函数头,其中有这样的字样:Java_com_example_hellojni_HelloJni_stringFromJNI,而com.example.hellojni恰好是例子工程的package name,HelloJni则是class的名称。也就是说,我们.so中函数声明涉及到的package name和class name与调用它的package name和class name不符。google一下印证了这个猜测,但提供现成解决办法的不多,总不能自己工程的package name必须和.so里的一样吧。
  搜索看到了一篇NDK的教程,Using NDK to Call C code from Android Apps(需翻墙),里面写明了在一个普通Android工程中,如何从零开始组装自己的native代码。大致的步骤是,首先写一个java类,里面声明一些需要实现native方法(函数签名即可),然后用javah命令来生成对应的header文件,里面的内容是自动生成的native函数声明,然后照着这个声明实现各个函数,最后编译,用最初的java类调用。
  受此启发,我们在拿到别人已经编译好的.so文件后,可首先新建一个java类,所在package的名称和class名称都与.so文件中函数签名提示的一致,在这个类中加入native方法的声明。这样在别处就可以用这个wrapper调用so库中的函数了。

在Android上利用NDK编译并使用Spatialite库(Windows环境)

  Sqlite数据库因其体积便携(基于单个文件,跨平台),功能完整(类似RDBMS)受到非常广泛的欢迎,在iOS,Android平台上都提供原生的支持。Spatialite是Sqlite的一个空间扩展,根据官方介绍,Spatialite之于Sqlite,相当于PostGIS之于PostgreSQL。
  Spatialite扩展提供了一系列用于空间操作的SQL函数,例如基础的空间查询,缓冲区生成等,也有高级的类似ArcObjects中ITopologicalOperator,IRelationalOperator等接口的功能。
  在Android平台上使用Spatialite资料比较少,主要是google上的Spatialite for Android和github上的android-spatialite两个工程。前者带有完整的编译步骤,我尝试了这个,其中遇到了些问题,记录下来,方便需要用到的朋友。后者工程似乎带有编译好的libjsqlite.so库,可直接使用。
  我机器的环境是Windows 7 64bit,已经搭建好了Android Eclipse开发环境。由于Spatialite及Sqlite是C/C++的native语言编写,而Android的默认开发语言是Java,所以在Android上编译Spatialite必须用到google提供的NDK(Native Development Kit)环境。NDK环境的搭建比较简单,按照google的文档进行即可:

  1. 下载NDK压缩包(android-ndk-r7b-windows.zip)。我下载的是最新的android-ndk-r7b版本。将其解压到任意目录,下文以<ndk>指代。注意:由于后面cygwin中配置环境变量时不支持带空格的路径,所以建议将其解压到某个盘符的根目录下,比如d:android-ndk-r7b;
  2. 设置Windows的环境变量。主要是将NDK的路径添加到PATH中去,方便在任何位置调用ndk-build命令。详细操作不再赘述。此步骤只是为cmd提供了便利;
  3. 由于NDK需求中说明,必须用到GNU Make命令,所以在Windows环境上还需要提前安装Cygwin。这是在Windows上模拟Linux环境的工具。安装步骤参考此贴:Windows环境下Android NDK环境搭建。注意在cygwin的linux环境下,配置$NDK环境变量,方便在任何地方调用ndk-build编译命令。我在安装Cygwin时,只额外勾选了Devel库。

  完成这个步骤后,可尝试编译运行NDK中自带的hello-jni例子,验证NDK编译环境已经搭好。具体步骤参见google的文档。可以发现,native代码需要放在与src同级的jni目录下,该目录下必须至少有Android.mk文件,向NDK系统描述源文件;编译时需要在工程的根目录下运<ndk>/ndk-build命令;利用NDK编译后,Android工程文件夹中会多出libs目录,里面会生成lib<something>.so共享库,这就是我们要用到类库,该文件也会一并打包到apk文件中去。更多的ndk知识请参考<ndk>/documentation.html文件,非常详尽。注:JNI相当于.net平台的p/invoke。
  接下来就要按照Spatialite for Android上的提示,在cygwin环境中编译spatialite库了。

  1. 在windows下利用svn工具对整个project checkout(这次作者用的是Spatialite 3.0.1,PROJ 4.7.0,geos 3.2.2几个库)。会发现有两个工程spatialite-android和spatialite-android-test。我们主要对spatialite-android工程进行编译;
  2. 由于spatialite用到了geos和proj两个库,所以首先需要为该工程下载,配置两个库。具体步骤如下,在cygwin环境中执行:
       1: $ cd jni

       2: $ wget http://download.osgeo.org/geos/geos-3.2.2.tar.bz2

       3: $ tar xvjf geos-3.2.2.tar.bz2

       4: $ cd geos-3.2.2

       5: $ ./configure --build=x86_64-pc-linux-gnu --host=arm-linux-eabi

       6: $ cd ..

       7: $ wget ftp://ftp.remotesensing.org/proj/proj-4.7.0.tar.gz

       8: $ tar xvzf proj-4.7.0.tar.gz

       9: $ cd proj-4.7.0

      10: $ ./configure --build=x86_64-pc-linux-gnu --host=arm-linux-eabi

      11: $ touch src/empty.cpp

      12: $ cd ..

  3. 按照作者的提示,下来就可以直接对工程进行ndk-build了(注:此时仍然在spatialite-android/jni目录下,而非spatialite-android目录)。如果你这么做了,会出现一个错误:
       1: ...

       2: Compile thumb  : proj <= rtodms.c

       3: Compile thumb  : proj <= vector1.c

       4: StaticLibrary  : libproj.a

       5: Compile++ thumb  : geos <= geos_c.cpp

       6: In file included from D:/spatialite-android/jni/geos-3.2.2/source/headers/geos/geom/Coordinate.h:20,

       7:                  from D:/spatialite-android/jni/geos-3.2.2/source/headers/geos/g                                                                                                                eom/Envelope.h:26,

       8:                  from D:/spatialite-android/jni/geos-3.2.2/source/headers/geos/i                                                                                                                ndex/strtree/STRtree.h:27,

       9:                  from D:/spatialite-android/jni/geos-3.2.2/capi/geos_c.cpp:19:

      10: D:/spatialite-android/jni/geos-3.2.2/source/headers/geos/platform.h:29:20: error                                                                                                                : ieeefp.h: No such file or directory

      11: /cygdrive/d/android-ndk-r7b/build/core/build-binary.mk:243: recipe for target `/                                                                                                                cygdrive/d/spatialite-android/obj/local/armeabi/objs-debug/geos/geos-3.2.2/capi/                                                                                                                geos_c.o' failed

      12: make: *** [/cygdrive/d/spatialite-android/obj/local/armeabi/objs-debug/geos/geos                                                                                                                -3.2.2/capi/geos_c.o] Error 1

    image
    提示ieeefp.h: No such file or directory。要修正这个错误,你需要在spatialite-android/jni/geos-3.2.2/source/headers/geos/platform.h文件中大约15行的位置,找到“#define HAVE_IEEEFP_H 1”这句话,并在其下面添加一行“#undef HAVE_IEEEFP_H”,取消对HAVE_IEEEFP_H的定义。保存该文件;

  4. 再次ndk-build,这回发现上面的错误消失了。但又产生了新的错误:
       1: ...

       2: Compile++ thumb  : geos <= InteriorPointLine.cpp

       3: Compile++ thumb  : geos <= InteriorPointPoint.cpp

       4: Compile++ thumb  : geos <= LineIntersector.cpp

       5: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp: In st                                                                                                                atic member function 'static double geos::algorithm::LineIntersector::interpolat                                                                                                                eZ(const geos::geom::Coordinate&, const geos::geom::Coordinate&, const geos::geo                                                                                                                m::Coordinate&)':

       6: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp:224: e                                                                                                                rror: expected unqualified-id before '(' token

       7: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp:232: e                                                                                                                rror: expected unqualified-id before '(' token

       8: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp: In me                                                                                                                mber function 'void geos::algorithm::LineIntersector::computeIntersection(const                                                                                                                 geos::geom::Coordinate&, const geos::geom::Coordinate&, const geos::geom::Coordi                                                                                                                nate&)':

       9: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp:304: e                                                                                                                rror: expected unqualified-id before '(' token

      10: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp:306: e                                                                                                                rror: expected unqualified-id before '(' token

      11: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp: In me                                                                                                                mber function 'int geos::algorithm::LineIntersector::computeIntersect(const geos                                                                                                                ::geom::Coordinate&, const geos::geom::Coordinate&, const geos::geom::Coordinate                                                                                                                &, const geos::geom::Coordinate&)':

      12: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp:426: e                                                                                                                rror: expected unqualified-id before '(' token

    image在很多位置都提示 error: expected unqualified-id before ‘(‘ token,google一下这个错误,发现大概是由于在头文件中对某个函数重复声明冲突引起的。于是打开spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp文件查看,发现基本都与一个名为“ISNAN”的函数有关。在Windows下搜索包含此字符串的文件,又来到了刚才修改过的platform.h文件中,发现这个文件中有与此函数相关的语句。为了解决此问题,尝试对此文件进行再次修改。在大约25行的位置,可以找到“/* #undef HAVE_ISNAN */”这句话,在这句话下面添加一句“#define HAVE_ISNAN 1”。这样做的目的是改变该文件后续的一些编译处理。

  5. 最后再次ndk-build,成功输出libjsqlite.so库。
    image

  现在我们在eclipse中运行spatialite-android这个工程(需要将test-2.3.sqlite数据库adb push到模拟器的sd卡上),就可以看到预期的结果了。
image  关于如何利用java语言调用spatialite的功能,可以详细参考这个工程中jsqlite包下的封装代码。至此我们就完成了在Android平台上使用spatialite库的准备工作。
  此次编译中遇到的问题可参考:http://code.google.com/p/spatialite-android/issues/detail?id=3

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

解读ArcGIS Runtime SDKs

  本文试图解读新的ArcGIS Runtime SDKs及其本质,与ArcGIS移动SDK,for iOS/Windows Phone/Android,之间的关系,以及这三种移动SDK后续发展的一些猜想。
  ArcGIS Runtime SDKs是随ArcGIS 10.1 beta一起发布的一套横跨桌面和移动端的,跨平台,轻量级的GIS开发SDK的总称。
image

  从上图中我们可以看出,ArcGIS Runtime SDKs家族包括了以下内容:

  • ArcGIS Runtime SDK for Android
  • ArcGIS Runtime SDK for iOS
  • ArcGIS Runtime SDK for Windows Phone
  • ArcGIS Runtime SDK for Windows Mobile
  • ArcGIS Runtime SDK for Java
  • ArcGIS Runtime SDK for Qt
  • ArcGIS Rutnime SDK for WPF

  之所以说ArcGIS Runtime SDKs横跨桌面和移动端,是因为它既包含了iOS/Windows Phone/Android等移动平台的开发SDK,也包含了可以开发传统桌面程序(C/S程序)的WPF、Java、QT等SDK;而后三种SDK则可开发出Windows和Linux平台下的具有丰富交互效果和良好体验的应用程序。其实ArcGIS Runtime还包括了一些现成的应用程序,比如iOS/Windows Phone/Android各自市场上都能下载到的“ArcGIS”程序,ArcGIS Mobile中的“ArcGIS”程序等。
  说起轻量级,首先要看看ArcGIS 10.1产品架构的一些变化。

image

  ArcGIS 10.1中,产品的划分更加明确和简单。主要分为四个部分,桌面GIS(传统的ArcMap,ArcGlobe等),ServerGIS(全新架构的ArcGIS Server),轻量级GIS(ArcGIS Explorer,Runtime SDKs及其对应的应用程序)和ArcGIS Online。前三部分都是围绕ArcGIS Online这个云GIS平台的,在不同程度上都与ArcGIS Online有交互,或可将数据直接部署上去,或作为它的客户端(云+端)。而轻量级GIS就是为了能够在任何地点,任何平台,任何设备上访问云平台提供的GIS能力。
  ArcGIS Runtime SDKs正是在这种背景下诞生的。其实它也分为狭义和广义上的两种解释。狭义的ArcGIS Runtime SDK是指桌面上的WPF,Java和Qt,它们的消息早在半年前就已经流出。它们的出现,是为了逐步替代强大而相对臃肿的ArcGIS Engine这个产品。做过Engine开发的朋友都知道,即使是最简单的显示地图的需求,理论上都必须在客户机上安装ArcGIS Engine Runtime这个运行时(注意不是ArcGIS Runtime),安装包通常400m左右。而利用新的ArcGIS Runtime SDK for WPF/Java/Qt开发出的程序,完全是绿色程序,不需要在客户机上安装任何部件(.Net Framework和JRE不计)即可运行,因为所有的依赖库直接和程序拷贝在一起即可。如果你喜欢ArcGIS Runtime SDK开发的程序的部署过程——拷贝到u盘里/插入目标计算机/运行,那么你一定也喜欢它的卸载过程——关闭程序/拔掉u盘。
  广义上的ArcGIS Runtime SDKs是上述列表中,诸多SDKs的统称。除了包含狭义的ArcGIS Runtime SDK,可以看到加入了移动端部分:ArcGIS Runtime SDK for iOS/Windows Phone/Android/Windows Mobile,而它们其实是新瓶装旧酒,分别对应以前的ArcGIS SDK for iOS/Windows Phone/Android和ArcGIS Mobile,只是换了产品名称而已。为什么会换名称?为什么还有ArcGIS Mobile这个“另类”的东西?这得从ArcGIS Runtime SDK的功能说起。
  做过ArcGIS Web API(ArcGIS API for Javascript/Flex/Silverlight)开发的朋友,应该可以很快理解狭义Runtime这个产品的所有功能。目前ArcGIS Runtime的功能与ArcGIS Web API,ArcGIS移动API(iOS/Windows Phone/Android)基本相同,都是基于ArcGIS REST API的。比如地图服务(动态/缓存)的加载,GraphicsLayer/FeatureLayer,基于FeatureService的数据编辑,Identify/Find/Query操作,GeometryService,Geoprocessing Service的调用等。它们的开发思路和代码编写几乎是一样的,比如ArcGIS API for Silverlight和目前的ArcGIS Runtime SDK for WPF,如出一辙(后者的前身就是ArcGIS API for WPF)。但狭义上的ArcGIS Runtime SDK与ArcGIS Web API不同之处在于,前者可以加载本地数据,包括Map Package/Tile Package/Locator Package/Geoprocessing Package。Map Package是包括.mxd文档和所有引用数据在内的压缩包,其余类似。最早的Package是9.3.1产品中的LayerPackage,它不仅包含了.lyr图层配置信息,还打包了图层所引用的实际数据。早期的ArcGIS Online平台允许用户上传LayerPackage以便分享,现在看来,直到此时Runtime产品的出现才将Package的概念发扬光大,并且为以后的所谓云GIS提供了更多的数据共享途径。对本地数据的读取是否破坏了基于ArcGIS REST API的框架呢?其实没有。ArcGIS API for WPF/Java/QT中,都内置了一个c++写的Web Server,读取本地数据后,会自动发布成这个Web Server上的REST服务来供Runtime SDK使用,所以一切功能,还是由REST API提供的。收起题外话,ArcGIS Runtime SDK的框架,是针对轻量级GIS产品(不包括ArcGIS Web API)的,它拥有统一的编程模型,可以用一致的开发思路,做出C/S,及移动端应用(Web API开发出B/S应用),为开发人员提供了极大的便利。这也是为什么将它们统称为ArcGIS Runtime SDKs的原因。
  在来说说ArcGIS Runtime for Windows Mobile(原来的ArcGIS Mobile)这个产品。之所以将它也归为ArcGIS Runtime SDK,是不无道理的。使用过ArcGIS Mobile的朋友一定知道,它的一个核心理念就是将数据划分为Basemap Layer和Operational Layer。前者是指起可视化参考作用的数据,一般是栅格底图或地图服务切片;后者是包含矢量信息在内的业务数据。ArcGIS Runtime中所涉及到Package,恰好与之对应。Tile Package对应Basemap Layer,Map Package对应Operational Layer。可以说,ArcGIS Mobile的设计理念,在整个ArcGIS Runtime产品家族中,得到了很好的延续。其实ArcGIS Mobile是个十分优秀的产品……
  到此,新的三个移动SDK(iOS/Windows Phone/Android)也归为ArcGIS Runtime SDKs的原因也就明了了,主要原因是都基于REST API的框架。另外基本可以肯定,它们都会使用相同的数据模型,即各种Package。由此不难推断,以后iOS/Windows Phone/Android平台上移动SDK的离线功能,也会依赖于Tile Package和Map Package。上周刚刚发布的ArcGIS API for iOS 2.1(以后要叫ArcGIS Runtime SDK for iOS)版本中,已经印证了这种猜测——加入了对Tile Package的支持,实现了原生的底图数据离线功能。其它两个移动平台的离线功能,敬请期待。
  由iPad这个产品引发的变革已经展开,移动设备数量正在爆炸性的增长,桌面设备和移动设备之间的概念会越来越模糊。Windows 8的发布预示着微软已经做出了改变,它即可以运行在桌面电脑上,也可以运行在平板设备中。有人说Windows 8的意义对于微软不亚于当年的Windows 95,我同意这个观点。Esri也做出了积极的改变,新的产品体系,新的产品命名,新的ArcGIS Runtime。

打个广告:2011 Esri中国开发者大会

http://developer.esrichina-bj.cn/

2011 Esri中国开发者大会将于6月16日北京开幕。

大会期间您将与来自全国各地的GIS开发者和GIS应用专家以及多位Esri中国工程师欢聚一堂,共同探讨和分享基于ArcGIS的应用开发技术,您将与众多国内业界同仁抢先接触ArcGIS 10的应用开发技术和技巧。此外,您还将聆听到其他与会者带来的成功经验和故事,与众多业界开发人员及Esri中国多位一线资深工程师充分讨论ArcGIS应用开发中遇到的技术问题。大会将为GIS技术决策者、开发者、IT专业人员奠定ArcGIS应用开发的成功基础。

大会期间,您将体验:

  • Esri当前技术及技术发展趋势
  • Esri技术专家带来的GIS开发思考与建议
  • ArcGIS开发平台及可用资源整体框架
  • ArcGIS 10新的产品许可授权机制与技术改进
  • 如何开发出高效的Geodatabase操作程序
  • 多元化的移动终端开发技术(ArcGIS API for iOS、Android、Windows Phone)
  • ArcGIS 富互联网应用(RIA)开发技术
  • ArcGIS Server高效生产力实现

  其中两个半主题由我来讲。对于大家比较关注的ArcGIS全新移动产品(iOS/Windows Phone/Android),将会涉及到完全离线应用,跨平台开发选择,产品发布计划等方面内容,欢迎大家关注。
  亲们,来就包两天四星级自助哦,ipad2,kindle等你拿哦:)

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/