Monthly Archives: April 2011

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

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

一、数据采集

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

二、查询和分析

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

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

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

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

clip_image002

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

   1: private bool _isActivated;

   2:         DispatcherTimer _dTimer;//use to control autoplay

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

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

   5:         private Slider _slider;//time slider

   6:         private TextBlock _textblock;//date textblock

   7:         private FeatureLayer _FLayer;//business layer

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

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

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

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

  12:         private enum SymbolSize

  13:         {

  14:             small=20,

  15:             middle=40,

  16:             large=60

  17:         }

  18:         public Map map1 { get; set; }

  19:         public bool IsActivated

  20:         {

  21:             get { return _isActivated; }

  22:             set

  23:             {

  24:                 if (_isActivated != value)

  25:                 {

  26:                     _isActivated = value;

  27:                     if (value)

  28:                     {                        

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

  30:                         _FLayer.Visible = false;

  31:                         map1.Layers.Add(_GLayerSymbol);

  32:                         map1.Layers.Add(_GLayerText);

  33:                         InitSymbols();

  34:                         _chkboxAutoPlay.Visibility = Visibility.Visible;

  35:                         _slider.ValueChanged += _slider_ValueChanged;

  36:                         map1.ExtentChanged += map1_ExtentChanged;

  37:                     }

  38:                     else

  39:                     {

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

  41:                         _FLayer.Visible = true;

  42:                         map1.Layers.Remove(_GLayerText);

  43:                         map1.Layers.Remove(_GLayerSymbol);

  44:                         UnInitSymbols();

  45:                         _chkboxAutoPlay.Visibility = Visibility.Collapsed;

  46:                         map1.ExtentChanged -= map1_ExtentChanged;

  47:                         

  48:                         _slider.ValueChanged -= _slider_ValueChanged;

  49:                         _chkboxAutoPlay.IsChecked = false;

  50:                         _dTimer.Stop();

  51:                     }

  52:                 }

  53:             }

  54:         }    

  55:  

  56: /// <summary>

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

  58:         /// </summary>

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

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

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

  62:         {

  63:             DoubleAnimation da = new DoubleAnimation();

  64:             da.Duration = duration;

  65:             if (visible == Visibility.Visible)

  66:             {

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

  68:                 da.From = 0;

  69:                 da.To = 100;

  70:             }

  71:             else

  72:             {

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

  74:                 da.From = 100;

  75:                 da.To = 0;

  76:             }

  77:             Storyboard sb = new Storyboard();

  78:             Storyboard.SetTarget(da, _Border);

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

  80:             sb.Children.Add(da);

  81:             sb.Begin();

  82:         }

  83:  

  84: private void InitSymbols()

  85:         {

  86:             foreach (Graphic g in _FLayer.Graphics)

  87:             {

  88:                 //determin symbol size

  89:                 double size = -1;

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

  91:                     size = (double)SymbolSize.small;

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

  93:                     size = (double)SymbolSize.middle;

  94:                 else

  95:                     size = (double)SymbolSize.large;

  96:  

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

  98:                 Graphic gsymbol = new Graphic()

  99:                 {

 100:                     Geometry = g.Geometry,

 101:                     Symbol = new PictureMarkerSymbol()

 102:                     {

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

 104:                     }

 105:                 };

 106:                 

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

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

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

 110:  

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

 112:                 Graphic gtext = new Graphic()

 113:                 {

 114:                     Geometry = g.Geometry,

 115:                     Symbol = new TextSymbol()

 116:                     {

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

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

 119:                         OffsetX = -size / 2+15,

 120:                         OffsetY = -size / 2+15,

 121:                         FontSize=30

 122:                     }

 123:                 };

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

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

 126:  

 127:                 _GLayerText.Graphics.Add(gtext);

 128:                 _GLayerSymbol.Graphics.Add(gsymbol); 

 129:             }

 130:             _GLayerText.Visible = false;

 131:         }

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

   1: public TimeQuery(Map map)

   2:         {

   3:             map1 = map;

   4:             _GLayerText = new GraphicsLayer();

   5:             _GLayerSymbol= new GraphicsLayer();

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

   7:             _slider = new Slider()

   8:             {

   9:                 Height=84,

  10:                 Width=360,

  11:                 Minimum=1,

  12:                 Maximum=30,

  13:                 SmallChange=1,

  14:                 LargeChange=5,

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

  16:             };

  17:  

  18:             _textblock = new TextBlock()

  19:             {

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

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

  22:                 FontSize=20,

  23:                 FontWeight=FontWeights.Bold,

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

  25:             };

  26:             StackPanel sp = new StackPanel()

  27:             {

  28:                 HorizontalAlignment=HorizontalAlignment.Center,

  29:                 VerticalAlignment=VerticalAlignment.Center,

  30:             };

  31:             sp.Children.Add(_slider);

  32:  

  33:             //auto play box

  34:             _chkboxAutoPlay = new CheckBox()

  35:             {

  36:                 Content = "连续查看",

  37:                 FontSize = 20,

  38:                 FontWeight=FontWeights.Bold,

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

  40:                 Visibility = Visibility.Collapsed,

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

  42:             };

  43:  

  44:             _dTimer = new DispatcherTimer()

  45:             {

  46:                 Interval = TimeSpan.FromMilliseconds(500)

  47:             };

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

  49:             {

  50:                 if (_slider.Value == 30)

  51:                     _slider.Value = 1;

  52:                 else

  53:                     _slider.Value += 1;

  54:             };

  55:  

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

  57:             {

  58:                 CheckBox chkbox = s as CheckBox;

  59:                 if (chkbox.IsChecked == true)

  60:                 {

  61:                     _dTimer.Start();

  62:                 }

  63:                 else

  64:                 {

  65:                     _dTimer.Stop();

  66:                 }

  67:             };

  68:             StackPanel sp1 = new StackPanel()

  69:             {

  70:                 Orientation = Orientation.Horizontal

  71:             };

  72:             sp1.Children.Add(_textblock);

  73:             sp1.Children.Add(_chkboxAutoPlay);

  74:             sp.Children.Add(sp1);

  75:  

  76:             _Border = new Border()

  77:             {

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

  79:                 Width=400,

  80:                 Height=0,

  81:                 CornerRadius= new CornerRadius(10),

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

  83:                 BorderThickness=new Thickness(5),

  84:                 //Visibility=Visibility.Collapsed,

  85:                 VerticalAlignment=VerticalAlignment.Top,

  86:             };

  87:             _Border.Child = sp;

  88:  

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

  90:  

  91:  

  92:             _salesmin = _salesmax = 0;

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

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

  95:             {

  96:                 string strDay = string.Empty;

  97:                 if (i < 10)

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

  99:                 else

 100:                     strDay = i.ToString();

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

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

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

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

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

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

 107:             }

 108:         }

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

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

   2:         {

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

   4:             //change text

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

   6:  

   7:             ChangeSymbolAndText(day);

   8:         }

   9:  

  10: /// <summary>

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

  12:         /// </summary>

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

  14:         private void ChangeSymbolAndText(int day)

  15:         {

  16:             string strDay = string.Empty;

  17:             if (day < 10)

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

  19:             else

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

  21:             

  22:             foreach (Graphic g in _FLayer.Graphics)

  23:             {

  24:                 //determin symbol size

  25:                 double size = -1;

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

  27:                     size = (double)SymbolSize.small;

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

  29:                     size = (double)SymbolSize.middle;

  30:                 else

  31:                     size = (double)SymbolSize.large;

  32:  

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

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

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

  36:  

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

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

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

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

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

  42:             }

  43:         }

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

   1: void map1_ExtentChanged(object sender, ExtentEventArgs e)

   2:         {

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

   4:                 _GLayerText.Visible = false;

   5:             else

   6:                 _GLayerText.Visible = true;

   7:         }

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

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

newnaw的一点改变

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

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