官方的的离线编辑终于隆重登场了,虽然目前还是测试版,但很多功能用起来还是很方便的,仔细研究了一下示例,还是挺有意思的,下面我带领大家来具体分析一下离线的示例OfflineEditor。
注:
1、官网示例用的是美国那边发布的服务,访问速度比较慢,本人截图时改成自己的本机服务了,效果差了点但功能是一致;
2、官方示例地址:https://developers.arcgis.com/android/sample-code/offline-editor/
3、示例源码下载地址:http://download.youkuaiyun.com/detail/arcgis_all/6990431
4、ArcGIS Runtime SDK for Android 10.2.2安装包下载地址:http://pan.baidu.com/s/1jGp33oy
5、ArcGIS Runtime SDK for iOS 10.2.2安装包下载地址:http://pan.baidu.com/s/1gdonHsv
1 OfflineEditor功能介绍
整个示例分为三部分内容:数据下载部分,离线编辑部分以及数据同步部分。
1.1 数据下载部分
数据下载部分提供了三个功能:刷新功能,下载功能,离线在线切换功能。
- 刷新功能:该功能主要是重新读取配置文件,并且删除原有的要素服务重新加载新的要素服务。
- 下载功能:该功能主要是获取服务的相关信息,并获取服务的要素数据,创建本地数据库,创建与服务相应的离线空间表,最后,删除了在线的要素服务图层,加载了离线的要素图层。
- 离线在线底图切换:该功能可切换不同类型的底图,默认加载在线底图,可切换成离线底图。
1.2 离线编辑部分
在这部分中我们就可以进行离线的要素编辑了,将示例切换到编辑界面,在这里我们可以编辑不同图层的要素,离线部分主要提供了图层切换,要素添加,要素属性编辑,要素删除等相关功能,效果如图:
- 图层切换:下载的离线数据可能会有多个图层,点击
可以列出所有的离线图层列表,如图:
- 图层切换:下载的离线数据可能会有多个图层,点击
服务只加了一个图层,因此列表里只有一个点要素层,选择要素图层,我们就可以对这个点要素图层进行编辑了。
- 添加要素:选择完要素图层操作后我们就可以添加要素,示例里要素是点要素图层,点击地图我们就可以添加一个点要素了,这只是添加在地图上了,而非保存到数据库里,添加点要素后,编辑菜单将有所变化,这时我们可以使用删除新添加的点要素功能和保存添加的点要素功能,如图:
- 要素属性编辑:我们可以对已有的要素进行要素编辑,长按地图上的点要素后,就会弹出要素的属性编辑窗体,如图:
在这个窗体里我们可以查看要素的属性信息,可以点击删除按钮,删除这个要素点,还可以编辑相关属性,点击编辑按钮后,属性字段进入编辑状态,这时您可以修改属性了。
1.3 数据同步
编辑好离线数据后我们可以切换到数据同步界面将编辑的数据同步到要素服务上,如图:
该部分就提供了一个功能:同步。点击图标后离线数据库里被编辑的数据都将会同步到要素服务器上。
到此示例的所有功能都介绍完了,下面我们来看看这些功能在代码里是怎样调用,怎样实现的。
2 OfflineEditor代码解析
- Java类主要用到四个类OfflineEditorActivity、GDBUtil、TemplatePicker和PopupForEditOffline。
- OfflineEditorActivity是我们应用程序的主界面,GDBUtil是数据的工具类,TemplatePicker是一个模板选择器,PopupForEditOffline是要素编辑类。
- 示例使用TabHost控件将应用分成三大功能部分:数据下载,离线编辑,数据同步。
2.1 配置的初始化
对于离线,在OfflineEditorActivity中的onCreate中主要是组装界面和初始化离线的配置文件,关键代码如下:
GDBUtil.loadPreferences(OfflineEditorActivity.this);
for (int i : GDBUtil.getLayerIds()) {
mapView.addLayer(new ArcGISFeatureLayer(GDBUtil.getFsUrl()+"/"
+ i,ArcGISFeatureLayer.MODE.ONDEMAND));
}
上面代码主要的工作是加载一些相关的配置,加载完后再将配置的要素服务加载到地图上进行显示。
我们来看一下GDBUtil.loadPreferences方法做哪些操作,在GDBUtil.java类中可看到代码如下:
publicstaticvoidloadPreferences(finalOfflineEditorActivity activity) {
SharedPreferences settings =PreferenceManager
.getDefaultSharedPreferences(activity);
setFsUrl(settings.getString("fsurl",DEFAULT_FEATURE_SERVICE_URL));
setGdbPath(Environment.getExternalStorageDirectory().getPath()
+ settings.getString("gdbfilename",DEFAULT_GDB_PATH));
setBasemapFileName(Environment.getExternalStorageDirectory().getPath()
+ settings.getString("tpkfilename",DEFAULT_BASEMAP_FILENAME));
setLayerIds(settings.getString("layerIds",DEFAULT_LAYERIDS));
setReturnAttachments(settings.getString("returnAttachments",
DEFAULT_RETURN_ATTACHMENTS));
setSyncModel(settings.getString("syncModel",DEFAULT_SYNC_MODEL));
}
上面代码主要做了下面这些事情:
- setFsUrl,设置要加载的要素服务的URL,DEFAULT_FEATURE_SERVICE_URL等参数在GDBUtil类中,在loadPreferences函数之赋值;
- setGdbPath,设置离线数据库的存储路径;
- setBasemapFileName,设置离线底图的存储路径;
- setLayerIds,设置要操作的图层编号;
- setReturnAttachments,设置是否有附件;
- setSyncModel,设置同步的方式;
到此,所有的相关配置都已经完成了。
2.2 数据下载
示例中提供了三个功能:刷新,下载,以及在线离线底图切换功能。
2.2.1 刷新功能
跟上面介绍的配置初始化基本相同,只不过是将原有的要素服务先移除后又重新加载的配置文件和初始化的要素图层。
2.2.2 下载功能
点击即可下载服务器上的数据了,下面看一下代码实现:
publicvoid downloadGdb(View view) {
GDBUtil.loadPreferences(OfflineEditorActivity.this);
if (getTemplatePicker() !=null) {
getTemplatePicker().clearSelection();
clear();
}
new MyAsyncTask().execute("downloadGdb");
}
上面代码在OfflineEditorActivity.java类中,是点击下载数据的按钮执行的方法,在这个方法里主要是重新加载了配置文件,清理模板选择器对象和执行一个异步任务请求,在这个异步类里实现了数据下载的代码,如下:
privateclassMyAsyncTaskextends AsyncTask<String, Void, Void> {
@Override
protected Void doInBackground(String...params) {
//TODO Auto-generated method stub
if (params[0].equals("syncGdb")) {
GDBUtil.synchronize(OfflineEditorActivity.this);
}elseif (params[0].equals("downloadGdb")) {
GDBUtil.downloadData(OfflineEditorActivity.this);
}
returnnull;
}
}
在上面代码的doInBackground方法中根据传入的不同的参数执行不同的操作,这里有两个操作:下载操作和同步操作。这次我们主要看下载的代码,在GDBUtil.downloadData方法中主要调用了downloadGeodatabase方法进行数据的下载,下面是方法的实现:
privatestaticvoiddownloadGeodatabase(
final OfflineEditorActivity activity,final MapView mapView) {
//通过该类获取featureService服务的相关信息和能力
gdbTask =new GeodatabaseTask(fsUrl,null,
new CallbackListener<FeatureServiceInfo>(){
@Override
publicvoid onError(Throwable e) {
//TODO Auto-generated method stub
Log.e(TAG,"", e);
showMessage(activity,e.getMessage());
showProgress(activity,false);
}
@Override
publicvoidonCallback(FeatureServiceInfo objs) {
//TODO Auto-generated method stub
if (objs.isSyncEnabled()){
requestGdbInOneMethod(gdbTask, activity, mapView);
}
}
});
}
在这里已经涉及到了10.2.1API中提供的离线相关类了,如上GeodatabaseTask类,这个类提供了丰富的功能GeodatabaseTask类的构造函数有三个参数:
- serviceUrl,要素服务FeatureService的URL地址
- UserCredentials,用户的安全凭证
- CallbackListener<FeatureServiceInfo>,回到函数的监听器。
GeodatabaseTask对象被初始化后会从服务器上获取服务的一些相关的服务信息创建FeatureServiceInfo对象,通过FeatureServiceInfo对象可以知道服务是否具备同步的能力,如果可以,同步者执行下面方法:
/**
* 'All-in-one' method.
*请求数据函数
*/
private static void requestGdbInOneMethod(GeodatabaseTaskgdbTask,
final OfflineEditorActivity activity,final MapView mapView) {
//请求数据时的参数
GenerateGeodatabaseParameters params =newGenerateGeodatabaseParameters(
GDBUtil.layerIds,mapView.getExtent(),
mapView.getSpatialReference(),GDBUtil.returnAttachments,
GDBUtil.syncModel,mapView.getSpatialReference());
showProgress(activity,true);
//gdb complete callback
CallbackListener<Geodatabase>gdbResponseCallback =new CallbackListener<Geodatabase>() {
@Override
publicvoid onCallback(Geodatabase obj) {
// update UI
showMessage(activity,"Geodatabase downloaded!");
Log.i(TAG,"geodatabase is: "+ obj.getPath());
showProgress(activity,false);
// remove all the feature layers from map and add a feature
// layer from thedownloaded geodatabase
for (Layer layer : mapView.getLayers()) {
if (layerinstanceofArcGISFeatureLayer)
mapView.removeLayer(layer);
}
for (GdbFeatureTable gdbFeatureTable :obj.getGdbTables()) {
if(gdbFeatureTable.hasGeometry())
mapView.addLayer(newFeatureLayer(gdbFeatureTable));
}
activity.setTemplatePicker(null);
}
@Override
publicvoid onError(Throwable e) {
Log.e(TAG,"", e);
showMessage(activity,e.getMessage());
showProgress(activity,false);
}
};
GeodatabaseStatusCallback statusCallback=newGeodatabaseStatusCallback() {
@Override
publicvoid statusUpdated(GeodatabaseStatusInfo status){
//TODO Auto-generated method stub
showMessage(activity,status.getStatus().toString());
}
};
// single method does it all!
gdbTask.submitGenerateGeodatabaseJobAndDownload(params,gdbFileName,
statusCallback,gdbResponseCallback);
showMessage(activity,"Submitting gdb job...");
}
这个方法涉及了下载数据的核心代码,下面我们来具体分析一下代码内容:
- GenerateGeodatabaseParameters,下载数据时所需的参数对象,该类构造函数一共有六个参数:
- layerIds,图层的id编号数组
- featureCacheExtent,下载数据的几何范围
- inSpatialRef,输入的坐标系
- returnAttachments,是否支持附件
- syncModel,数据的同步模式
- outSpatialRef,输出的坐标系
- CallbackListener<Geodatabase>,完成GDB数据库下载的回调函数类,在该回调中我们只可以执行一些操作,如示例里在回调中删除了在线的服务图层,加载离线的数据图层到地图上进行显示。通过Geodatabase本地数据库可以获取要素图层列表List<GdbFeatureTable>对象,通过newFeatureLayer(gdbFeatureTable)来创建一个离线要素图层进行要素显示。
- GeodatabaseStatusCallback,本地数据库回调状态类,在数据下载过程中会有很多状态改变,各种状态改变时都会走这个类的回调函数。
- GeodatabaseTask.submitGenerateGeodatabaseJobAndDownload,通过该方法生成离线数据库和相应的要素表,方法需要传递上面介绍的三个参数和一个数据库存储的路径。
- GenerateGeodatabaseParameters,下载数据时所需的参数对象,该类构造函数一共有六个参数:
到此,下载数据的过程已经解析完成,核心类GeodatabaseTask,调用他的submitGenerateGeodatabaseJobAndDownload方法即可实现离线数据包的生成。
2.2.3 在线离线底图切换
功能比较简单,点击按钮来切换底图,代码如下,要点已经添加注释,不过多阐述了。
publicvoidswitchLocalBasemap(View view) {
GDBUtil.loadPreferences(OfflineEditorActivity.this);
if (switchBasemapbutton.isChecked()) {
//读取tpk文件
File file =new File(GDBUtil.getBasemapFileName());
//判断是否存在
if (file.exists()) {
//如果存在移除底图,添加离线底图
mapView.removeLayer(0);
try {
mapView.addLayer(
newArcGISLocalTiledLayer(GDBUtil
.getBasemapFileName()),0);
GDBUtil.showMessage(OfflineEditorActivity.this,
"Local Basemap is On");
}catch (Exception e) {
//TODO: handle exception
switchBasemapbutton.setChecked(false);
GDBUtil.showMessage(OfflineEditorActivity.this,
"Invalid Basemap Tpk File");
}
}else {
switchBasemapbutton.setChecked(false);
GDBUtil.showMessage(OfflineEditorActivity.this,
"Local Basemap tpk doesn't exist");
}
}else {
//如果不存在移除底图添加新的在线底图
mapView.removeLayer(0);
mapView.addLayer(
newArcGISTiledMapServiceLayer(
"http://map.geoq.cn/ArcGIS/rest/services/ChinaOnlineCommunity/MapServer"),
0);
GDBUtil.showMessage(OfflineEditorActivity.this,
"Local Basemap is Off. Switched to ArcGIS Online Basemap");
}
}
2.3 离线数据编辑
切换到编辑界面,即可进行编辑下载的数据了,点击编辑按钮进行图层的选择,之后即可针对不同的要素图层来编辑数据。
下面我们来看看编辑相关的代码,该端代码也在OfflineEditorActivity.java类里面。点击按钮弹出一个类似一个图例的选择窗体,代码如下:
publicvoideditButton(View view) { GDBUtil.showProgress(OfflineEditorActivity.this,true);
clear();
int layerCount = 0;
for (Layer layer :mapView.getLayers()) {
if (layerinstanceof FeatureLayer) {
layerCount++; }
}
if (layerCount > 0) {
if (myListener ==null){
myListener =new MyTouchListener(OfflineEditorActivity.this, mapView);
mapView.setOnTouchListener(myListener);
}
if (getTemplatePicker() !=null) {
getTemplatePicker().showAtLocation(editButton, Gravity.BOTTOM, 0, 0);
}else {
new TemplatePickerTask().execute();
}
} else {
GDBUtil.showMessage(OfflineEditorActivity.this, "No Editable Local Feature Layers.");
}
GDBUtil.showProgress(OfflineEditorActivity.this,false);
}
点击该按钮主要做了两个工作,一是给mapView添加了Touch监听器,二是创建一个TemplatePicker窗体;先来看看TemplatePicker控件,他是扩展PopupWindow的子类,主要用于展示离线的图层列表,如图:
怎么扩展的这个窗体感兴趣的朋友可以自己了解一下源码,我们的重点放在如何编辑离线数据,弹出上面窗体后选择一个图层符号即可进行这个图层的要素编辑。
点击地图即可在地图上添加要素点,由于为mapView添加了Touch监听器,点击后会调用onSingleTap方法,因为添加的是点要素直接来看一下添加点的代码,如下:
if(editingmode==POINT){
GdbFeature g;
try {
graphicsLayer.removeAll();
// this needs totbe created from FeatureLayer
// by
// passing template
g =((GdbFeatureTable) ((FeatureLayer)mapView
.getLayerByID(getTemplatePicker()
.getSelectedLayer().getID()))
.getFeatureTable())
.createFeatureWithTemplate(
getTemplatePicker()
.getselectedTemplate(),
point);
Symbol symbol= ((FeatureLayer)mapView
.getLayerByID(getTemplatePicker()
.getSelectedLayer().getID()))
.getRenderer().getSymbol(g);
Graphic gr =newGraphic(g.getGeometry(),
symbol,g.getAttributes());
addedGraphicId =graphicsLayer.addGraphic(gr);
}catch (TableException e1) {
//TODO Auto-generated catch block
e1.printStackTrace();
}
points.clear();
}
上面的流程很简单,主要如下:
- 通过mapView获取FeatureLayer图层,
- 通过FeatureLayer获取GdbFeatureTable对象,
- 通过GdbFeatureTable创建一个GdbFeature对象,
- 通过FeatureLayer得到Symbol对象,
- 创建Graphic对象,并且添加到图层上显示。
点击按钮将移除刚才添加的要素点,点击
时将会把新添加的要素点保存到离线的数据库中,下面我们来看看新点要素是如何别保存到本地数据库的,代码如下:
if(editingmode==POINT)
try {
addedGraphic =graphicsLayer.getGraphic(addedGraphicId);
((FeatureLayer)mapView.getLayerByID(getTemplatePicker()
.getSelectedLayer().getID())).getFeatureTable()
.addFeature(addedGraphic);
graphicsLayer.removeAll();
}catch (TableException e1) {
//TODO Auto-generated catch block
e1.printStackTrace();
}
else {
……
核心上面是FeatureTable的addFeature方法,通过这个方法即可将新添加的要素添加的本地数据库中,FeatureTable除这个方法还有许多其他的方法,如下:
- addFeatures,一次添加多个要素到离线数据库中
- deleteFeature,删除本地要素
- deleteFeatures,删除多个本地要素
- queryFeatures,查询本地要素
- updateFeature,更新要素到本地
- updateFeatures更新多个要素到本地
添加要素看完了,下面我们来看看示例里编辑要素属性是怎么实现的。
通过示例代码发现,示例里要素的编辑非常方便,可以直接使用,长按地图调用onLongPress方法,即可弹出要素编辑框,重点如下:
popup = newPopupForEditOffline(mapView,
OfflineEditorActivity.this);
popup.showPopup(e.getX(), e.getY(), 25);
弹出框的窗体主要是PopupForEditOffline类维护的,通过showPopup方法展示窗体,代码如下:
publicvoid showPopup(floatx,floaty,inttolerance) {
if (!map.isLoaded())
return;
// Instantiate a PopupContainer
popupContainer =new PopupContainer(map);
int id =popupContainer.hashCode();
popupDialog =null;
// Display spinner.
if (progressDialog ==null|| !progressDialog.isShowing())
progressDialog =ProgressDialog.show(map.getContext(),"",
"Querying...");
// Loop through each layer in thewebmap
// Envelopeenv = newEnvelope(map.toMapPoint(x, y), tolerance
// *map.getResolution(), tolerance * map.getResolution());
Layer[] layers =map.getLayers();
count =new AtomicInteger();
for (Layer layer : layers) {
// If the layer has not been initialized or is invisible, do
// nothing.
if (!layer.isInitialized() ||!layer.isVisible())
continue;
if (layerinstanceof FeatureLayer) {
// Query feature layer and displaypopups
FeatureLayer localFeatureLayer= (FeatureLayer) layer;
if (localFeatureLayer.getPopupInfos() !=null) {
// Query feature layer which is associated with apopup
// definition.
count.incrementAndGet();
newRunQueryLocalFeatureLayerTask(x, y, tolerance, id)
.execute(localFeatureLayer);
}
}
}
}
看到上面的代码你是不是感觉很熟悉呢,它跟我们的PopupInWebMapForEditing示例如出一辙,只不过她的图层信息是通过服务获取的,而离线图层的信息是通过FeatureLayer图层获取的,再通过PopupContainer容器将要素信息显示出来,展示要素信息的代码如下:
//获取图层弹出信息对象是否为空
Map<Integer,ArcGISPopupInfo> popupInfos =localFeatureLayer
.getPopupInfos();
if (popupInfos ==null) {
// Dismiss spinner
if (progressDialog !=null&&progressDialog.isShowing()
&&count.intValue() == 0)
progressDialog.dismiss();
return;
}
for (Feature fr : features) {
//创建弹出对象
Popuppopup =localFeatureLayer.createPopup(map, 0, fr);
popup.setEditable(true);
popupContainer.addPopup(popup);
}
counter++;
if (counter < 2) {
//添加编辑工具
createEditorBarLocal(localFeatureLayer,true);
}
//创建弹出窗体
createPopupViews(id);
通过PopupContainer展示要素的属性信息非常方便,只需创建相应的Popup对象即可,前提是我们的获得的离线图层信息对象ArcGISPopupInfo不能为空,创建要素编辑步骤如下:
- 创建PopupContainer对象。
- 判断离线图层的ArcGISPopupInfo是否为空。
- 创建Popup对象,并将该对象添加到PopupContainer容器里。
- 创建编辑工具条createEditorBarLocal()。
- 展示要素信息createPopupViews。
属性展示效果图如下:
编辑完属性后调用FeatureTable的updateFeature方法将数据更新到数据库里即可。
2.4 数据同步
点击按钮即可将数据上传到数据库中,我们来看看具体的实现代码:
staticvoidsynchronize(final OfflineEditorActivityactivity) {
showProgress(activity,true);
gdbTask =new GeodatabaseTask(fsUrl,null,
new CallbackListener<FeatureServiceInfo>(){
@Override
publicvoid onError(Throwable e) {
//TODO Auto-generated method stub
Log.e(TAG,"", e);
showMessage(activity,e.getMessage());
showProgress(activity,false);
}
@Override
publicvoidonCallback(FeatureServiceInfo objs) {
//TODO Auto-generated method stub
if (objs.isSyncEnabled()){
doSyncAllInOne(activity);
}
}
});
}
通过上面代码可以了解到,再执行上传前我们还是要先通过GeodatabaseTask对象来获取服务信息,之后再同步到服务上去,同步代码如下:
privatestaticvoiddoSyncAllInOne(final OfflineEditorActivityactivity) {
try {
// create localgeodatabase
Geodatabase gdb =new Geodatabase(gdbFileName);
// get sync parameters fromgeodatabase
final SyncGeodatabaseParameters syncParams = gdb
.getSyncParameters();
CallbackListener<Geodatabase>syncResponseCallback =new CallbackListener<Geodatabase>() {
@Override
publicvoid onCallback(Geodatabase objs) {
showMessage(activity,"Sync Completed");
showProgress(activity,false);
Log.e(TAG,"Geodatabase: "+ objs.getPath());
}
@Override
publicvoid onError(Throwable e) {
Log.e(TAG,"", e);
showMessage(activity,e.getMessage());
showProgress(activity,false);
}
};
GeodatabaseStatusCallbackstatusCallback =newGeodatabaseStatusCallback() {
@Override
publicvoid statusUpdated(GeodatabaseStatusInfo status) {
//TODO Auto-generated method stub
showMessage(activity, status.getStatus().toString());
}
};
// start sync...
gdbTask.submitSyncJobAndApplyResults(syncParams,gdb,
statusCallback,syncResponseCallback);
}catch (Exception e) {
e.printStackTrace();
}
}
上面是同步的代码了,我们来看看同步的步骤:
- 创建离线数据库对象Geodatabase;
- 创建同步的参数对象SyncGeodatabaseParameters;
- 创建同步数据库的回调对象CallbackListener<Geodatabase>;
- 创建同步数据库的状态回调对象GeodatabaseStatusCallback;
- 通过GeodatabaseTask对象执行submitSyncJobAndApplyResults方法进行数据同步。
同步完成后,我们可以通过服务路径来查看一下数据是否真的成功了,如果网络正常的话在你的服务里可以看到你上传数据信息。
到此,整个离线示例解析完了,如有问题可以共同探讨。