这一篇章我们来加载各种文件,在开始之前我们先固定一个加载的顺序,不然胡乱把kml,tpk,shp加载进来可能会出现图层错误覆盖的问题。第一层是tpk,第二层是kml,第三层就是shp或者是featureLayer的要素图层。例如图中巨大范围的tpk正射影像,如果不是第一层就加载进来,而是把kml或者shp等要素图层加载了进来然后再加载tpk,那么你的tpk就会把前面的图层给错误的覆盖掉什么也看不到了。

这一章增加了新的LauncherActivity,在程序启动的时候会有零点几秒的白屏,可以在这个白屏的时间展示一张图片来美化一下。并且同时进行申请动态权限,安卓6.0之后不能单单在清单文件中写上权限系统就给你的,而是需要动态弹窗授权,很多新人对安卓不是很懂,直接复制代码即使是正确的也加载不出来,很大几率是动态权限的问题。

在LauncherActivity启动的时候也帮我们在存储主目录创建一个和程序名字一样的目录,叫ArcgisAndroid,然后在这个目录中创建3个目录,分别是kml,tpk,shp。但是shp有点不一样,shp是由多个文件组成一个整体的。最好用一个新的目录包住这些shp文件然后再复制进shp目录,如图

还记得程序的侧边栏有加载kml,tpk,shp的选择吗?点击它弹窗多选确定后即可添加入mapView,代码如下

binding.navView.setNavigationItemSelectedListener {
	when(it.itemId){
		R.id.nav_addkml -> {
			showChoiceDialog(getString(R.string.kml), getString(R.string.kmlMapJson), getString(R.string.menu_addkml)) {
				homeViewModel.loadKmlToMapView()
			}
		}
		R.id.nav_addshp -> {
			showChoiceDialog(getString(R.string.shp), getString(R.string.shpMapJson), getString(R.string.menu_addshp)) {
				homeViewModel.loadShpToMapView()
			}
		}
		R.id.nav_addtpk -> {
			showChoiceDialog(getString(R.string.tpk), getString(R.string.tpkMapJson), getString(R.string.menu_addtpk)) {
				homeViewModel.loadTpkToMapView()
			}
		}
		R.id.nav_viewpoint -> {
			val mapView = binding.mapView
			val layerMap = linkedMapOf<String,Layer>()
		   mapView.map.operationalLayers.map {
				if(it is GroupLayer){
					it.layers.forEach {
						layerMap[it.name] = it
					}
				}
			}
			val singleItems = layerMap.keys.toTypedArray()
			var checkedItem = -1
			MaterialAlertDialogBuilder(requireContext())
				.setTitle(getString(R.string.menu_dingwei))
				.setPositiveButton(resources.getString(R.string.ok)) { dialog, which ->
					closeDrawerLayout()
					val layer = layerMap[singleItems[checkedItem]]
					mapView.setViewpointAsync(Viewpoint(layer?.fullExtent), 0.5f)
				}
				.setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
					checkedItem = which
				}
				.show()
		}
	}
	true
}


/**
 * 复用一下弹窗选择的逻辑,例如 fileFolder是kml,遍历得到所有的文件转成map,key就是文件名value就是文件的绝对路径。
 * 勾选完毕之后就得到了一组 key-value的内容了,然后存进rememberMap转成为json存储在sp中
 * 到第二次打开弹窗的时候判断文件名在不在rememberMap中,就知道是否选中了。
 */
private fun showChoiceDialog(fileFolder:String,fileTypeJsonKey:String,title:String, okOnClickListener: View.OnClickListener){
	//根据目录来遍历得到所有的文件
	val fileFolder = Environment.getExternalStorageDirectory().absolutePath + File.separator + resources.getString(R.string.app_name) + File.separator + fileFolder
	File(fileFolder).apply {
		if(!exists()) { mkdirs() }
		val fileNamePathMap = listFiles()?.associateBy({it.name},{it.absolutePath})
		val rememberMap = LinkedHashMap<String?,String?>()
		val fileNameList = fileNamePathMap?.keys?.toTypedArray()
		val mapJson = SPUtils.getInstance().getString(fileTypeJsonKey)
		if(!TextUtils.isEmpty(mapJson)){
			val map = Gson().fromJson(mapJson, Map::class.java)
			map.entries.forEach {
				rememberMap[it.key.toString()] = it.value.toString()
			}
		}
		val checkedItems: BooleanArray?
		if(rememberMap.isEmpty()){
			checkedItems = fileNamePathMap?.map { false }?.toBooleanArray()
		} else {
			checkedItems = fileNamePathMap?.map {
				rememberMap.containsKey(it.key)
			}?.toBooleanArray()
		}
		MaterialAlertDialogBuilder(requireContext())
			.setTitle(title)
			.setPositiveButton(R.string.ok) { dialog, which ->
				val mapJson = Gson().toJson(rememberMap)
				SPUtils.getInstance().put(fileTypeJsonKey, mapJson)
				closeDrawerLayout()
				okOnClickListener.onClick(null)
			}
			.setMultiChoiceItems(fileNameList, checkedItems) { dialog, which, checked ->
				// Respond to item chosen
				val name = fileNameList?.get(which)
				if(checked) {
					rememberMap[name] = fileNamePathMap?.get(name)
				} else {
					rememberMap.remove(name)
				}
			}.show()
	}
}
showChoiceDialog方法复用一下弹窗选择的逻辑,例如 fileFolder是kml,遍历得到所有的文件转成map,key就是文件名value就是文件的绝对路径。多选之后就得到了一组 文件名-文件绝对路径 的内容了,然后存进rememberMap转成为json存储在sp中,到第二次打开弹窗的时候判断文件名在不在rememberMap中就知道是否选中了。

binding.navView.setNavigationItemSelectedListener 点击侧边栏弹窗多选后回调,分别调用3个homeViewModel方法,这里homeViewModel的作用就是把过多的业务逻辑分离一下,不然HomeFragment代码会越来越多。在HomeFragment初始化的时候就把mapView引用给了homeViewModel了

package com.tanqidi.arcgisandroid.ui.viewModel

import androidx.lifecycle.ViewModel
import com.esri.arcgisruntime.data.ShapefileFeatureTable
import com.esri.arcgisruntime.data.TileCache
import com.esri.arcgisruntime.geometry.Envelope
import com.esri.arcgisruntime.layers.ArcGISTiledLayer
import com.esri.arcgisruntime.layers.FeatureLayer
import com.esri.arcgisruntime.layers.GroupLayer
import com.esri.arcgisruntime.layers.KmlLayer
import com.esri.arcgisruntime.mapping.Viewpoint
import com.esri.arcgisruntime.mapping.view.MapView
import com.esri.arcgisruntime.ogc.kml.KmlDataset
import com.google.gson.Gson
import com.tanqidi.arcgisandroid.App
import com.tanqidi.arcgisandroid.R
import com.tanqidi.arcgisandroid.utils.SPUtils
import com.tanqidi.arcgisandroid.utils.toast
import io.reactivex.rxjava3.core.Single
import java.io.File

class HomeViewModel : ViewModel() {

    lateinit var mapView: MapView

    fun loadTpkToMapView(){
        val groupLayer = GroupLayer()
        getMapFromSPUtilsByResourceId(R.string.tpkMapJson)?.forEach {
            val fileName = it.key.toString()
            val filePath = it.value.toString()

            val tileCache = TileCache(filePath)
            val arcGISTiledLayer = ArcGISTiledLayer(tileCache)
            //把name也设置给layer,这样后续图层定位可以用到
            arcGISTiledLayer.name = fileName
            groupLayer.layers.add(arcGISTiledLayer)
        }
        addGroupLayerToMapViewOperationalLayers(groupLayer,0)
    }

    fun loadKmlToMapView(){
        val groupLayer = GroupLayer()
        getMapFromSPUtilsByResourceId(R.string.kmlMapJson)?.forEach {
            val fileName = it.key.toString()
            val filePath = it.value.toString()

            val kmlDataset = KmlDataset(filePath)
            val kmlLayer = KmlLayer(kmlDataset)
            //把name也设置给layer,这样后续图层定位可以用到
            kmlLayer.name = fileName
            groupLayer.layers.add(kmlLayer)
        }
        addGroupLayerToMapViewOperationalLayers(groupLayer, 1)
    }

    // TODO: 待开发跳转功能,不然加载进来shp等资源不知道在什么位置
    fun loadShpToMapView(){
        val groupLayer = GroupLayer()
        val asyncJobList = mutableListOf<Single<Any>>()

        //用最后的shp的Envelope作为缩放
        var fullExtent:Envelope? = null
        getMapFromSPUtilsByResourceId(R.string.shpMapJson)?.forEach {
            val fileName = it.key.toString()
            //这里得到的filePath其实是shp的父目录,需要得到父目录下面的xxx.shp加载进来就行了
            val filePath = it.value.toString()

            File(filePath).listFiles()?.filter { it.name.contains(".shp") }?.forEach {
                val fileName = it.name
                val absolutePath = it.absolutePath

                //异步加载,添加进来
                val asyncJob = Single.create<Any> { emitter ->
                    val shapefileFeatureTable = ShapefileFeatureTable(absolutePath)
                    shapefileFeatureTable.addDoneLoadingListener {
                        val featureLayer = FeatureLayer(shapefileFeatureTable)
                        groupLayer.layers.add(featureLayer)
                        //把name也设置给layer,这样后续图层定位可以用到
                        featureLayer.name = fileName
                        //shp范围
                        fullExtent = featureLayer.fullExtent

                        emitter.onSuccess(true)
                    }
                    shapefileFeatureTable.loadAsync()
                }
                asyncJobList.add(asyncJob)
            }
        }
        //全部异步任务完成后再添加进来
        Single.zip(asyncJobList) { objects -> objects }.subscribe({
            addGroupLayerToMapViewOperationalLayers(groupLayer, 2)
            // TODO: 临时跳转过去看效果,fullExtent是最后一个shp位置的范围
            mapView.setViewpointAsync(Viewpoint(fullExtent))
        }, {
            it.message?.let { it1 -> toast(it1) }
            //是空的,也清空掉
            addGroupLayerToMapViewOperationalLayers(groupLayer, 2)
        })

    }

    private fun getMapFromSPUtilsByResourceId(sourceId:Int) : Map<*, *>? {
        val mapJson = SPUtils.getInstance().getString(App.app!!.getString(sourceId))
        return Gson().fromJson(mapJson, Map::class.java)
    }

    private fun addGroupLayerToMapViewOperationalLayers(groupLayer: GroupLayer, index:Int) {
        mapView.map?.operationalLayers?.apply {
            //取消勾选,或者再次勾选都会再次执行
            try {
                removeAt(index)
            } catch (e: Exception) {
                //e.printStackTrace()
            }
            add(index, groupLayer)
        }
    }
}

加载kml和tpk是一样的,但是为什么调用getMapFromSPUtilsByResourceId呢,因为在弹窗选择完毕的时候会得到一组(文件名-文件绝对路径)的map内容并且转成了json存在了sp中,这里是重新拿出来将json转成map,然后就可以循环遍历了,key就是文件名value就是文件的绝对路径,然后就可以创建加载kml或者tpk的对应图层对象了。

这里我还使用了GroupLayer图层组,例如我加载5个tpk,一次性创建好然后放进GroupLayer中,最后把GroupLayer添加进mapView就可以了。不然你每创建就往mapView中添加,感觉不是很优雅~

 加载shp的时候稍微有点不一样,因为shp是由很多个文件组成的,在弹窗的时候选中的只是这个shp的父目录而已

 需要过滤得到里面的.shp文件才是真正要加载的文件,然后用rxjava异步任务等待,等所有的需要加载的shp都加载完毕后,再将groupLayer添加进mapView。

最后就是addGroupLayerToMapViewOperationalLayers方法了

 这里index就是添加的下标了,tpk传的是0,kml是1,shp是2,后续的featureLayer直接add不用管下标就是自动下标+1这就解决图层覆盖错乱的问题,自此加载tpk,kml,shp就做好了。

还有图层定位功能,所有图层new出来加入GroupLayer的时候都把文件名赋值了

 所以在这里直接遍历mapView.map.operationalLayers,如果是GroupLayer继续遍历,存在map中,key是文件名,value是layer。这是个单选功能,选择完毕得到layer的fullExtent,然后用mapView.setViewpointAsync(Viewpoint(layer?.fullExtent), 0.5f)就能将地图跳转缩放到该图层的上方了

 arcgis指北针,读者自行看代码

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

Logo

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

更多推荐