在前面章节的讲述中,我们完成了shp,geojson,geodatabase的存储模式,不过这只能将数据存储在本地无法有效统一的管理数据,只能算是个单机版程序。在这一章节中我们将使用featureServer服务来将我们的要素信息存储在服务端,以达到一端编辑多端实时更新的效果。

还记得我们在项目配置的图层添加的界面吗?我在这里做了判断是否是服务地址的条件,如果是featureServer网络地址将弹窗多选来勾选,以快速来生成本地配置项目.json图层内容

多勾选之后直接就能得到该featureServer下的featureLayer中的所有字段信息了,这样就满足了我们程序的项目规划要求了

因为是自动生成的,每个图层里面都包含有featureLayerUrl地址,在侧边栏中把存储模式设置成为以featureServer存储即可读取主项目中的所有图层,然后得到每个图层下面的featureLayerUrl地址,然后使用ServiceFeatureTable开始连接

建议各位不要将资源设置为公共读写这样不安全,强烈建议各位使用账号密码方式来访问资源

val serviceFeatureTable = ServiceFeatureTable(featureServerUrl)
serviceFeatureTable.credential = UserCredential("admin","123456")

/**
 * featureServer 数据操作
 */
private fun loadProjectFeatureServerToMapView() {
	val mainProjectName = SPUtils.getInstance().getString(App.app!!.getString(R.string.main_project))
	val project = projectRepository.getProjectByName(mainProjectName)

	val groupLayer = GroupLayer()
	val asyncJobList = mutableListOf<Single<Any>>()
	val featureLayerUrlList = project.layers.filter { !TextUtils.isEmpty(it.featureLayerUrl) }
	featureLayerUrlList.forEachIndexed { index, layerConfig ->
		val job = Single.create<Any> { emit->
			val serviceFeatureTable = ServiceFeatureTable(layerConfig.featureLayerUrl)
			serviceFeatureTable.requestConfiguration

			serviceFeatureTable.addLoadStatusChangedListener {
				when(it.newLoadStatus){
					LoadStatus.LOADED -> {
						val displayName = serviceFeatureTable.displayName
						featureTable[if(displayName.contains("_")){
							displayName.split("_")[1]
						} else {
							displayName
						}] = serviceFeatureTable

						val featureLayer = FeatureLayer(serviceFeatureTable)
						groupLayer.layers.add(featureLayer)
						//开始符号化
						SymbolUtils.symbol(layerConfig, featureLayer)
						featureLayer.name = displayName
						emit.onSuccess(true)
					}
					else -> {}
				}
			}
			serviceFeatureTable.loadAsync()
		}
		asyncJobList.add(job)
	}
	//全部异步任务完成后再添加进来
	Single.zip(asyncJobList) { objects -> objects }.subscribe({
		addGroupLayerToMapViewOperationalLayers(groupLayer, MAP_PROJECT_FEATURE_INDEX)
		resourceLoadStatusLiveData.postValue(true)
	}, {
		it.printStackTrace()
		it.message?.let { it1 -> showToast(it1) }
	})
}

读取完毕后,添加,编辑,查询,删除要素的操作和之前是一样的,值得一提的是featureServer中的featureLayer图层服务不一定支持附件操作,这取决于你在arcgis server中发布图层的时候是否选择开启附件功能,如果不开启这里的在线模式和后面的离线生成geodatabase也一样不支持附件操作,这一点希望读者能明白

记得在使用arcgis server发布featureServer的时候开启支持离线模式,不然是无法使用离线功能的。不出意外你已经成功操作到了这一步,已经能够把你的featureServer中的featureLayer加载进了程序点击主页右上角菜单,点击开启离线等待进度条完毕即可进入到 featureServer离线存储模式,想要把本地添加和编辑或者删除后的要素同步上去就点击同步数据即可,接下来是代码部分

先说离线部分,首先我们项目通过一个https://192.186.1.1/xx/xx/xx/FeatureServer来识别出来了N个图层,featureServer/0,/1,/N是featureServer里面的某个featureLayer子图层,所有的要素都是存在featureLayer中的子图层的,需要先拿到https://192.186.1.1/xx/xx/xx/FeatureServer然后通过它来进行离线,我这里所有的ServiceFeatureTable都存在featureTable全局map变量中,通过调用values然后用map操作符再进行类型转换成ServiceFeatureTable得到url将最后的/n编号去掉,再toSet()去重复就能得到一个featureServer服务地址了

假设一个featureServer服务里面有多达100个featureLayer,采用默认的离线规则不合适,离线完成的时候它会生成一个Geodatabase里面会有100个GeodatabaseFeatureTable表,这会很大程度影像我们的离线速度,在这种情况下我们就需要按需离线。

例如在前面我们多选自动生成的图层中,需要离线featureLayer的id为8,12,23,50的4个表,默认parameters.layerOptions里面有100个id,先清空然后再放进来这4个id,这个时候它就只会离线生成这4个GeodatabaseFeatureTable,速度和性能这样就上去了。可以看到featureLayerIdList就是从主项目中得到所有的图层配置里面的url在得到最后的id,来进行按需离线,此乃最佳实践

/**
 * featureServer离线模式,开始生成geodatabase和各种配置
 */
val generateGeodatabaseJobList = mutableListOf<GenerateGeodatabaseJob>()
fun featureServerOnlineToOffline() {
	generateGeodatabaseJobList.clear()
	//创建进度条弹窗
	val createProgressDialog = createProgressDialog("离线中...")
	//默认把确定禁止点击,失败失败才能点击
	createProgressDialog.accept.apply {
		isEnabled = false
		setOnClickListener {
			currentShowDialog?.dismiss()
		}
	}
	//进度条中如果中途想要取消,可以使用这个来进行取消job任务
	createProgressDialog.cancel.setOnClickListener {
		generateGeodatabaseJobList.forEachIndexed { index, generateGeodatabaseJob ->
			generateGeodatabaseJob.cancel()
		}
		//关闭dialog
		currentShowDialog?.dismiss()
	}
	//离线的屏幕范围
	val extent = mapView.visibleArea.extent
	//异步等待结果
	val asyncJobList = mutableListOf<Single<Any>>()
	//从featureTable得到所有的featureServer排重一下
	val featureServerList = featureTable.values.map {
		val uri = (it as ServiceFeatureTable).uri
		val substring = uri.substring(0, uri.lastIndexOf("/"))
		substring
	}.toSet()
	val offlineDatabasePath = Environment.getExternalStorageDirectory().absolutePath + File.separator + getString(R.string.app_name) + File.separator + getString(R.string.project_data) + File.separator + getString(R.string.offline)
	featureServerList.forEachIndexed { index, featureServerUrl ->
		val job = Single.create<Any> { emit ->
			val geodatabaseSyncTask = GeodatabaseSyncTask(featureServerUrl)
			geodatabaseSyncTask.addDoneLoadingListener {
				// create generate geodatabase parameters for the current extent
				var progressView: ItemProgressBarBinding?
				val defaultParameters = geodatabaseSyncTask.createDefaultGenerateGeodatabaseParametersAsync(extent)
				defaultParameters.addDoneListener {
					progressView = ItemProgressBarBinding.inflate(LayoutInflater.from(mapView.context), createProgressDialog.progressBarContainer, true)
					try {
						val parameters = defaultParameters.get()
						/**
						 * 假设一个FeatureServer里面有多达100个FeatureLayer,采用默认的离线规则不合适,离线完成的时候它会生成一个Geodatabase里面会有100个GeodatabaseFeatureTable表
						 * 这会很大程度影像我们的离线速度,在这种情况我们就需要按需离线,例如我需要离线FeatureLayer的Id为8,12,23,50的4个表,默认parameters.layerOptions里面有100个id,先
						 * 清空然后再放进来这4个id,这个时候它就只会离线生成这4个GeodatabaseFeatureTable,速度和性能这样就上去了
						 */
						val mainProjectName = getSPUtils().getString(getString(R.string.main_project))
						val mainProject = projectRepository.getProjectByName(mainProjectName)
						val featureLayerIdList = mainProject.layers.map { it.id }
						parameters.layerOptions.apply {
							clear()
							featureLayerIdList.forEachIndexed { index, id ->
								add(GenerateLayerOption(id.toLong()).apply {
									//whereClause = "如果在离线的时候,可以过滤指定的离线要素"
								})
							}
						}
						//开启附件,离线将会支持附件操作,这里表示离线下载的要素也会把附件下载下来,同时也支持离线编辑要素
						parameters.isReturnAttachments = true
						//将当前的url找到对应的layer作为名字
						val localGeodatabasePath = "$offlineDatabasePath/${mainProjectName}.geodatabase"
						// create and start the job
						val generateGeodatabaseJob = geodatabaseSyncTask.generateGeodatabase(parameters, localGeodatabasePath)
						generateGeodatabaseJobList.add(generateGeodatabaseJob)
						//查看进度
						progressView!!.title.text = mainProjectName
						generateGeodatabaseJob.addProgressChangedListener {
							createProgressDialog.cancel.isEnabled = true

							progressView!!.progressBar.progress = generateGeodatabaseJob.progress
							progressView!!.progressTextView.text = "${generateGeodatabaseJob.progress}%"
						}
						// get geodatabase when done
						generateGeodatabaseJob.addJobDoneListener {
							if (generateGeodatabaseJob.status == Job.Status.SUCCEEDED) {
								emit.onSuccess(index)
							} else {
								progressView!!.error.text = "离线失败,请重试"
								emit.onError(RuntimeException("离线失败,请重试"))
							}
						}
						//开始!
						generateGeodatabaseJob.start()
					} catch (e: Exception) {
						e.printStackTrace()
						emit.onError(RuntimeException("离线失败,请重试"))
						progressView?.error?.text = "离线失败,请重试"
					}
				}
			}
			geodatabaseSyncTask.loadAsync()
		}
		asyncJobList.add(job)
	}
	//等待
	Single.zip(asyncJobList) { objects -> objects }.subscribe({
		//将范围写入json
		getSPUtils().apply {
			put(getString(R.string.extent), extent.toJson())
			//将存储模式设置成为storage_feature_server_offline,这个只能通过离线成功来设置,不能通过侧边栏变更存储模式来设置
			put(getString(R.string.storage_type), getString(R.string.storage_feature_server_offline))
		}
		//关闭dialog
		currentShowDialog?.dismiss()
		//数据库生成完毕
		onlineOrOfflineSwitchLiveData.postValue(true)
		MaterialAlertDialogBuilder(mapView.context)
			.setTitle("提示")
			.setCancelable(false)
			.setMessage("存储模式切换成功")
			.setPositiveButton("立即重启") { a,b ->
				com.blankj.utilcode.util.AppUtils.relaunchApp(true)
			}
			.show()

	}, {
		createProgressDialog.accept.isEnabled = true
		//如果有3个数据库,成功了2个,失败了1个,依然全部删除
		File(offlineDatabasePath).listFiles()?.forEachIndexed { index, file ->
			FileUtil.deleteFile(file)
		}
		//失败了
		onlineOrOfflineSwitchLiveData.postValue(false)
	})
}

离线成功后会在 ArcgisAndroid》项目数据 》离线模式》 中生成xxxx.geodatabase数据库文件,前面已经做了对geodatabase的各种crud了我这里就不再详解了,在离线成功后会自动进入 <featureServer离线模式>,这个时候你无法切换存储模式,需要把数据同步上去了才会自动切换到 <featureServer存储模式>,离线模式和使用geodatabase存储模式代码是一样的,只是读取的路径不一样

 

好啦,接下来就是同步数据的部分了没啥好讲解的,我在同步完成的时候对离线出来到同步完成的整个结果xxxxx.geodatabase做一次归档备份到同目录的 数据备份 目录,不仅仅是featureServer上面是最新的数据,同时我们外业调查得到的最终结果也要留一份作为凭证,里面保留着你所有的要素和图片附件,最好是上传到云端备份哪怕是featureServer意外丢失了数据它也能保你一命!

/**
 * 同步
 */
val syncGeodatabaseJobList = mutableListOf<SyncGeodatabaseJob>()
fun featureServerOfflineToOnline(){
	syncGeodatabaseJobList.clear()
	//创建进度条弹窗
	val createProgressDialog = createProgressDialog("同步中...")
	//默认把确定禁止点击,失败失败才能点击
	createProgressDialog.accept.apply {
		isEnabled = false
		setOnClickListener {
			currentShowDialog?.dismiss()
		}
	}
	createProgressDialog.cancel.setOnClickListener {
		syncGeodatabaseJobList.forEachIndexed { index, syncGeodatabaseJob ->
			syncGeodatabaseJob.cancel()
		}
		currentShowDialog?.dismiss()
		onlineOrOfflineSwitchLiveData.postValue(true)
	}

	// create parameters for the sync task
	val syncGeodatabaseParameters = SyncGeodatabaseParameters()
	syncGeodatabaseParameters.syncDirection = SyncGeodatabaseParameters.SyncDirection.BIDIRECTIONAL
	syncGeodatabaseParameters.isRollbackOnFailure = false

	val offlineDatabasePath = Environment.getExternalStorageDirectory().absolutePath + File.separator + getString(R.string.app_name) + File.separator + getString(R.string.project_data) + File.separator + getString(R.string.offline)
	//同步异步
	val asyncJobList = mutableListOf<Single<Any>>()
	File(offlineDatabasePath).listFiles()?.forEachIndexed { index, file ->
		val job = Single.create<Any> { emitter ->
			val geodatabase = Geodatabase(file.absolutePath)
			geodatabase.addDoneLoadingListener {
				if (geodatabase.loadStatus == LoadStatus.LOADED) {
					// get the layer ID for each feature table in the geodatabase, then add to the sync job
					geodatabase.geodatabaseFeatureTables.forEach { geodatabaseFeatureTable ->
						val serviceLayerId = geodatabaseFeatureTable.serviceLayerId
						val syncLayerOption = SyncLayerOption(serviceLayerId)
						syncGeodatabaseParameters.layerOptions.add(syncLayerOption)
					}
					val geodatabaseSyncTask = GeodatabaseSyncTask(geodatabase.serviceUrl)
					val syncGeodatabaseJob = geodatabaseSyncTask.syncGeodatabase(syncGeodatabaseParameters, geodatabase)
					syncGeodatabaseJobList.add(syncGeodatabaseJob)
					syncGeodatabaseJob.start()
					//查看进度
					val progressView = ItemProgressBarBinding.inflate(LayoutInflater.from(mapView.context), createProgressDialog.progressBarContainer, true)
					progressView.title.text = file.name.split(".")[0]
					syncGeodatabaseJob.addProgressChangedListener {
						createProgressDialog.cancel.isEnabled = true

						progressView.progressBar.progress = syncGeodatabaseJob.progress
						progressView.progressTextView.text = "${syncGeodatabaseJob.progress}%"
					}
					syncGeodatabaseJob.addJobDoneListener {
						if (syncGeodatabaseJob.status == Job.Status.SUCCEEDED) {
							emitter.onSuccess(true)
						} else {
							progressView.error.text = "同步失败,请重试"
							emitter.onError(RuntimeException("同步失败,请重试"))

						}
					}
				}
			}
			geodatabase.loadAsync()
		}
		asyncJobList.add(job)
	}

	//等待
	Single.zip(asyncJobList) { objects -> objects }.subscribe({
		getSPUtils().apply {
			//移除离线标识,只需要把存储模式设置成为  featureServer存储模式 即可
			put(getString(R.string.storage_type), getString(R.string.storage_feature_server))
			//去掉范围框json
			remove(getString(R.string.extent))
		}
		//在mapView中去掉那个红色离线框
		mapView.graphicsOverlays.clear()

		//将数据库文件备份到本地
		val backupsFolder = Environment.getExternalStorageDirectory().absolutePath + File.separator + getString(R.string.app_name) + File.separator + getString(R.string.project_data) + File.separator + getString(R.string.backups)
		val format = SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒")
		 File(offlineDatabasePath).listFiles()?.forEachIndexed { index, file ->
			FileUtil.copyFile(file, File(backupsFolder, format.format(Date())+".geodatabase").absolutePath)
			file.delete()
		}
		//关闭dialog
		currentShowDialog?.dismiss()
		//同步成功
		onlineOrOfflineSwitchLiveData.postValue(true)

		MaterialAlertDialogBuilder(mapView.context)
			.setTitle("提示")
			.setCancelable(false)
			.setMessage("存储模式切换成功")
			.setPositiveButton("立即重启") { a,b ->
				com.blankj.utilcode.util.AppUtils.relaunchApp(true)
			}
			.show()
	}, {
		createProgressDialog.accept.isEnabled = true
		onlineOrOfflineSwitchLiveData.postValue(true)
	})
}

 

写在最后我遇到了一个无法解决的异常问题,在程序不重启的情况下反复同步和离线,到第二次离线成功他就无法编辑和添加要素了出现了下图的异常信息。所以我只能绕过去了每次同步成功和离线成功都强制让程序来一次自杀重启来规避,如果你有更好的解决方案欢迎留言与我探讨!

https://gitee.com/tanqidi/ArcgisAndroidhttps://gitee.com/tanqidi/ArcgisAndroid

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐