移动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的父类,TiledMapServiceLayer或TiledLayer,来实现自己的自定义图层,比如用它来加载Google Maps,天地图等各种地图。加载这些在线地图都是通过重写GetTileUrl()方法来实现的。
对于已经存放在硬盘上的缓存文件,该如何加载呢?这几个图层还有一个方法,GetTileSource。这个方法有一个onComplete action,可以传入ImageSource类型的参数,它比GetTileUrl来的更直接。其实GetTileSource方法中调用了GetTileUrl方法的结果(一个获得tile的url字符串),利用这个字符串向服务器端发送请求,请求回来的结果就是切片图片的二进制流,再将这个二进制流形成ImageSource,通过onComplete方法返回。
所以我们可以抛开GetTileUrl,直接重写GetTileSource方法,来根据row,col,level参数,读取地图服务的缓存文件。首先将Exploded格式的地图服务缓存文件拷贝到手机中:
包含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张切片。切片文件更少,主要目的是为了迁移方便。文档中并未给出读取这种格式文件的方法,不过牛魔王已经凭空推断出了这种格式的内容,这里就借鉴了他的方法。还是先将缓存文件拷贝到手机中:
利用conf.cdi和conf.xml获得tiling scheme,之后重写GetTileSource方法。具体思路牛魔王文中已经给出,感兴趣的同学还是看原文,学习牛牛的思路比较好。
下面是读取两种缓存文件的效果:
3、第三方离线地图文件
除了ArcGIS Server的缓存切片之外,我们还可以读取第三方的离线地图文件来做为我们的底图。比如以前面介绍过的Mobile Atlas Creator为例,我现在已经有了很多自己下载好的离线地图,如果能在ArcGIS移动客户端使用起步两全其美?其实在目前的离线导航软件中,很多都用sqlite数据库做为地图存储格式,因为它应用广泛,轻巧,紧凑,Android,iOS,Symbian等系统对它都有原生的支持。Mobile Atlas Creator中,RMaps和OruxMaps都用Sqlite保存离线地图。这里以应用较为广泛的RMaps格式为例,进行试验。
创建好的RMaps地图文件如下:
我们利用FireFox里的Sqlite Manager插件先来查看一下数据库的内容:
可以看出,我们所需的内容都保存在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叠加的效果:
需要说明的是,不论是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同样适用。
Pingback: ArcGIS移动客户端业务数据离线方案的讨论 - 菩提老王的葡萄架
Pingback: ArcGIS移动客户端业务数据离线方案的讨论 « 懒得折腾
Pingback: ArcGIS移动客户端中可以自动离线的底图图层 - 菩提老王的葡萄架
你好 能不能给一个重写GetTileSource的类的全部代码??
一个完整的例子可以参考这个:
http://www.arcgis.com/home/item.html?id=d2b40d7f553947a2b575556b057f5dcf
或者用reflector看一下api里原来的代码。
老王,啥时写一篇关于地图纠偏的文章?让我们拜读一下。
坐标偏移方面确实没什么研究,网上其他前辈应该写过不少,可以找找看。有心得肯定和大家分享:)
老王,你好:
我现在开发mobile正需要这个代码,第一次弄mobile的开发,对此不太熟悉,请麻烦你给我传一份这个例子的代码,不胜感激。
能给我传一下例子中WP_LocalCacheReader的源码吗?谢谢
供参考。
base.Initialize();时报Index was outside the bounds of the array.这个异常,请问这个怎摸处理啊谢谢
请问楼主有源码吗?我弄了好久,但是地图显示不出来,不知道为什么?
android中继承TiledServiceLayer ,实现protected byte[] getTile(int lev, int col, int row) 对谷歌地图的访问。这个我怎么也搞不出来。听楼主的意思应该特别简单的,我是新手楼主能否实现以下把代码贴出来。小弟万分感谢。
我没做过android,但肯定同理可行。给你找了两个参考:
http://forums.arcgis.com/threads/51194-Offline-database-backed-tile-layer
http://datamoil.blogspot.com/2011/06/offline-tiled-layer-with-arcgis-for.html
希望你在明白原理后去做,这样也好发现问题所在。
我的事例,但是感觉在胡闹,地图根本就不显示。
package com.esri.arcgis.android.samples.helloworld;
import java.util.concurrent.RejectedExecutionException;
import android.util.Log;
import com.esri.android.map.TiledLayer;
import com.esri.android.map.TiledServiceLayer;
import com.esri.core.geometry.Envelope;
import com.esri.core.geometry.GeometryEngine;
import com.esri.core.geometry.Point;
import com.esri.core.geometry.SpatialReference;
public class CustomTiledMapServiceLayer extends TiledServiceLayer{
/****/
private TileInfo _titlinfo;
private static final double c[] = {
4891.96981024998D, 2445.98490512499D, 1222.99245256249D, 611.49622628138D, 305.748113140558D, 152.874056570411D, 76.4370282850732D, 38.2185141425366D, 19.1092570712683D, 9.55462853563415D,
4.77731426794937D
};
//scale
private static final double d[] = {
18489297.737236D, 9244648.868618D, 4622324.434309D, 2311162.217155D, 1155581.108577D, 577790.554289D, 288895.277144D, 144447.638572D, 72223.819286D, 36111.909643D,
18055.954822D
};
public CustomTiledMapServiceLayer() {
super(“”);
Log.v(“ddddddddddd”,”CustomTiledMapServiceLayer”);
try{
if (true) {
try {
getServiceExecutor().submit(new Runnable() {
final CustomTiledMapServiceLayer a;
public final void run() {
a.initLayer();
}
{
a = CustomTiledMapServiceLayer.this;
}
});
return;
}
catch (RejectedExecutionException _ex) { }
}
}catch(Exception e){
e.printStackTrace();
}
}
protected byte[] getTile(int lev, int col, int row) throws Exception {
Log.v(“ddd”,”getTile”);
String url = “http://mt”+(col%4)+”.google.cn/vt/lyrs=t@128,r@174000000&hl=zh-CN&gl=cn&src=app&x=”+col+”&y=”+row+”&z=”+lev+”&s=”;
return url.getBytes();
}
@Override
protected void initLayer() {
Log.v(“dddddddddd”,”initLayer”);
try {
setTileInfo(new com.esri.android.map.TiledServiceLayer.TileInfo(new Point(-20037508.342787D, 20037508.342787D),
d, c, 11, 96, 256, 256));
Envelope envelope1 = (Envelope)GeometryEngine.project(getInitialExtent(), SpatialReference.create(102113), SpatialReference.create(102113));
setInitialExtent(envelope1);
Envelope envelope = (Envelope)GeometryEngine.project(getInitialExtent(), SpatialReference.create(102113), SpatialReference.create(102113));
setInitialExtent(envelope);
this.create();
super.initLayer();
return;
}
catch (Exception exception) {
exception.printStackTrace();
Log.v(“dddddd”,”Exception”);
}
}
/****/
@Override
public Envelope getFullExtent() {
Log.v(“dddd”,”getFullExtent”);
return new Envelope(-20037508.342787, -20037508.342787, 20037508.342787, 20037508.342787);
}
@Override
protected Envelope getInitialExtent() {
Log.v(“dddd”,”getInitialExtent”);
return new Envelope(11505945.4433426, 4033034.1810582, 11772432.8383941, 4320403.23966665);
}
@Override
public SpatialReference getSpatialReference() {
Log.v(“dddd”,”getSpatialReference”);
return SpatialReference.create(102113);
}
}
我的qq:1239832360 请赐教
老师您好,我是一个在校学生,对GIS很感兴趣。现在手机分辨率越来越高,我之前做的切图显示的时候,显示的时候,字都特别小,请问您知道用什么方法可以获取适合高分辨率手机的mbtiles地图数据么?谢谢了。
1、现在获取mbtiles的地图推荐用这个工具:https://geopbs.codeplex.com/
2、关于字小问题产生的原因和解决办法见:http://resources.arcgis.com/en/help/windows-phone-sdk/concepts/011v/011v00000013000000.htm
能认识一下你么
留过言就已经认识了:)
老王你好 能共享一下第二种方案的代码 牛魔王的Flex代码不懂
https://blog.newnaw.com/?p=736&cpage=1#comment-2416
评论里有时也有有用的信息。。。
老王 还想请教你一下 WP_LocalCacheReader 这个类如何使用
我 myMap.Layers.Add(new LocalCacheLayer(path));之后
什么都没显示出来
这个类是一个示例代码,很可能不能直接应用到你的工程中。但里面有了实现的基本思路和关键技术,你可以结合搜索引擎,对立面代码进行理解,之后就好排查为什么你的地图出不来了。
老王你好,我能问问呢tilerow和tilecol是什么吗?为什么有的是128,有的是256,有的又是512呢?
https://blog.newnaw.com/?p=69
看看这个吧,供参考
老王你好,如果方便的话,能不能加一下qq,指导我一下,QQ:799172467,十分感谢!