ArcGIS Runtime Android 开发总结

5,790 阅读25分钟

最近在项目中使用到了ArcgisRuntime Android,因此打算做一份总结,既加深了自己对该部分知识的印象,也可以方便不熟悉的同学参考。本文使用的版本是arcgis-android:100.6.0',示例部分既有参照官方文档API介绍,也有参考 总有刁民想杀寡人的专栏,当然更多的还是自己使用的一些心得。下面开始正文介绍

  • Arcgis入门示例
  • 定位相关
  • 编辑地图
  • 常用接口
  • 天地地图接入
  • 三维地图
  • 热像图

一、Arcgis入门示例

标准地图

布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.vincent.arcgisdemo.ui.MainActivity">

    <com.esri.arcgisruntime.mapping.view.MapView
        android:id="@+id/mapView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff"/>

</FrameLayout>

代码

class MainActivity :AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        EasyAndroid.init(this)
        
        val levelOfDetail = 16
        val map = ArcGISMap(
            Basemap.Type.TOPOGRAPHIC, 30.671475859566514, //纬度
            104.07567785156248,//精度
            levelOfDetail//缩放级别(只能设置,不能获取,且必须大于0)
        )
        mapView.map = map
    }
    override fun onResume() {
        mapView.resume()
        super.onResume()
    }
    override fun onPause() {
        mapView.pause()
        super.onPause()
    }
    override fun onDestroy() {
        mapView.dispose()
        super.onDestroy()
    }
}

设置地图背景颜色

val mainBackgroundGrid = BackgroundGrid()
// 设置背景颜色
mainBackgroundGrid.color = -0x1
// 设置背景格子线颜色
mainBackgroundGrid.gridLineColor = -0x1
// // 设置背景格子线宽度 单位(dp)
mainBackgroundGrid.gridLineWidth = 0f

mapView.backgroundGrid = mainBackgroundGrid

显示Arcgis基础地图

val levelOfDetail = 16
        val map = ArcGISMap(
            Basemap.Type.TOPOGRAPHIC, 30.671475859566514, //纬度
            104.07567785156248,//精度
            levelOfDetail//缩放级别(只能设置,不能获取,且必须大于0)
        )
        mapView.map = map

加载基础地图的图层

val url = "https://www.arcgis.com/home/item.html?id=7675d44bb1e4428aa2c30a9b68f97822"
map.basemap.baseLayers.add(ArcGISTiledLayer(url))

添加地图绘制监听

// 添加地图状态改变监听
        mapView.addDrawStatusChangedListener {
            // 地图绘制完成 另一种就是绘制中 DrawStatus.IN_PROGRESS
            if (it.drawStatus == DrawStatus.COMPLETED) {
                // 开始定位
                initLocal()
            }
        }

地图放大缩小

注意,此操作是异步。

 // 放大地图
iv_add.setOnClickListener {
    mapView.setViewpointScaleAsync(mapView.mapScale * 0.5)
}
// 缩小地图
iv_reduction.setOnClickListener {
    mapView.setViewpointScaleAsync(mapView.mapScale * 2)
}

二、定位相关

既然是地图,那么一定会涉及到定位,接下来的内容就是关于定位的相关内容。

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        mapView.map = map
        initLocal()
    }
    
private fun initLocal() {
    // 申请运行时权限(此处申请定位即可)
    EasyPermissions.create(
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.ACCESS_FINE_LOCATION)
        .callback {
            if(it){
                // 开始定位
                val mLocationDisplay = mapView.locationDisplay
                // 定位显示的模式
                mLocationDisplay.autoPanMode = LocationDisplay.AutoPanMode.RECENTER
                // 开始异步定位
                mLocationDisplay.startAsync()
            }else{
                EasyToast.DEFAULT.show("请打开相关所需要的权限,供后续测试")
            }
        }
        .request(this)
}

默认定位
其中定位显示模式有如下四种:

  • COMPASS_NAVIGATION 步行导航

当用户步行时,位置符号被固定在屏幕上的特定位置,并且总是指向设备的顶部边缘,最适合于waypoint导航。

  • NAVIGATION 车载导航

最适合车内导航,位置符号固定在屏幕上的特定位置,并且始终指向屏幕顶部。

  • OFF

用户位置符号会随位置变化而移动,但地图不会动

  • RECENTER

当位置符号移动到“漂移范围”之外时,通过重新调整位置符号的中心,将位置符号保留在屏幕上。(第三方解释:当用户位置处于当前地图范围内时候,用户位置符号会随位置变化而移动,但地图不会动;当用户位置处于地图边缘时候,地图会自动平移是用户的当前位置重新居于显示地图中心。)

如果接下来需要获取当前定位点的经纬度信息,还要修改marker呢?接下来再看:

val mLocationDisplay = mapView.locationDisplay
mLocationDisplay.autoPanMode = LocationDisplay.AutoPanMode.RECENTER
mLocationDisplay.startAsync()
val pinStarBlueDrawable =
    ContextCompat.getDrawable(this, R.mipmap.icon_marker_blue) as BitmapDrawable?
// 地图图形图像
val campsiteSymbol = PictureMarkerSymbol.createAsync(pinStarBlueDrawable).get()
mLocationDisplay.addLocationChangedListener {event ->
    // 查看返回的定位信息
    EasyLog.DEFAULT.e(event.location.position.toString())
    // 修改默认图标
    mLocationDisplay.defaultSymbol = campsiteSymbol
    // mLocationDisplay.isShowLocation = false//隐藏符号
    mLocationDisplay.isShowPingAnimation = false//隐藏位置更新的符号动画
}

看看地址信息:

定位日志

我们看到的是定位是每三秒一次定位,这个频率还是挺高的。如果我们只需要定位一次呢?那在定位成功以后关闭定位即可:

if (mLocationDisplay.isStarted)
    mLocationDisplay.stop()

注意:

经过多次测试,发现关闭定位是不受控制的:在红米手机测试时,定位成功以后无法关闭,使用OFF定位模式以后,发现直接定位失败;使用红米4X测试时,发现OFFRECENTER下测试都能成功定位,但是始终无法关闭定位;小米9测试结果和红米4X一致。最终在自定义位置显示marker的期望没有实现,且最终发生了下面的异常:

2019-09-29 19:59:29.027 2795-8055/com.vincent.arcgisdemo A/art: art/runtime/indirect_reference_table.cc:137] JNI ERROR (app bug): weak global reference table overflow (max=51200)
2019-09-29 19:59:29.027 2795-8055/com.vincent.arcgisdemo A/art: art/runtime/indirect_reference_table.cc:137] weak global reference table dump:
2019-09-29 19:59:29.027 2795-8055/com.vincent.arcgisdemo A/art: art/runtime/indirect_reference_table.cc:137] please find table dump in dropbox: 2795-weak global reference-table-overflow-dump
...
2019-09-29 19:59:29.421 2795-8055/com.vincent.arcgisdemo A/art: art/runtime/runtime.cc:423]   at com.vincent.arcgisdemo.ui.MainActivity.showMarker(MainActivity.kt:113)
2019-09-29 19:59:29.421 2795-8055/com.vincent.arcgisdemo A/art: art/runtime/runtime.cc:423]   at com.vincent.arcgisdemo.ui.MainActivity.access$showMarker(MainActivity.kt:23)
2019-09-29 19:59:29.421 2795-8055/com.vincent.arcgisdemo A/art: art/runtime/runtime.cc:423]   at com.vincent.arcgisdemo.ui.MainActivity$initLocal$1$1.onLocationChanged(MainActivity.kt:95)

源码:

class MainActivity : AppCompatActivity() {

    private val mGraphicsOverlay = GraphicsOverlay()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(com.vincent.arcgisdemo.R.layout.activity_main)
        EasyAndroid.init(this)
        val mainBackgroundGrid = BackgroundGrid()
        mainBackgroundGrid.color = -0x1
        mainBackgroundGrid.gridLineColor = -0x1
        mainBackgroundGrid.gridLineWidth = 0f

        mapView.backgroundGrid = mainBackgroundGrid
        val levelOfDetail = 16

        val map = ArcGISMap(
            Basemap.Type.TOPOGRAPHIC, 30.671475859566514, //纬度
            104.07567785156248,//精度
            levelOfDetail//缩放级别(只能设置,不能获取,且必须大于0)
        )
//
        val url = "https://www.arcgis.com/home/item.html?id=7675d44bb1e4428aa2c30a9b68f97822"
        map.basemap.baseLayers.add(ArcGISTiledLayer(url))

//        val map = ArcGISMap(Basemap(ArcGISVectorTiledLayer(url)))
//        val vp = Viewpoint(47.606726, -122.335564, 72223.819286)
//        map.initialViewpoint = vp
        mapView.map = map
        // 添加地图状态改变监听
        mapView.addDrawStatusChangedListener {
            // 绘制完成 另一种就是绘制中 DrawStatus.IN_PROGRESS
            if (it.drawStatus == DrawStatus.COMPLETED) {
                initLocal()
            }
        }


        mapView.graphicsOverlays.add(mGraphicsOverlay)


    }

    /**
     * 开始监听
     * 官方文档是需要下面两种权限,示例中是三种权限,区别在于 Manifest.permission.ACCESS_COARSE_LOCATION
     */
    private fun initLocal() = EasyPermissions.create(
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION
    )
        .callback {
            if (it) {
                val mLocationDisplay = mapView.locationDisplay
                mLocationDisplay.autoPanMode = LocationDisplay.AutoPanMode.RECENTER
                mLocationDisplay.startAsync()
                val pinStarBlueDrawable =
                    ContextCompat.getDrawable(
                        this,
                        com.vincent.arcgisdemo.R.mipmap.icon_marker_blue
                    ) as BitmapDrawable?
                val campsiteSymbol = PictureMarkerSymbol.createAsync(pinStarBlueDrawable).get()
                mLocationDisplay.addLocationChangedListener { event ->
                    // 查看返回的定位信息
                    EasyLog.DEFAULT.e(event.location.position.toString())
                    mLocationDisplay.defaultSymbol = campsiteSymbol
//                    mLocationDisplay.isShowLocation = false//隐藏符号
//                    mLocationDisplay.isShowPingAnimation = false//隐藏位置更新的符号动画
                    if (mLocationDisplay.isStarted) {
                        mLocationDisplay.stop()
                    }

                    showMarker(event.location.position.x, event.location.position.y)


                }
            } else {
                EasyToast.DEFAULT.show("请打开相关所需要的权限,供后续测试")
            }
        }
        .request(this)


    private fun showMarker(x: Double, y: Double) {
        EasyLog.DEFAULT.e("x${x}  y${y}")
        val pinStarBlueDrawable =
            ContextCompat.getDrawable(
                this,
                com.vincent.arcgisdemo.R.mipmap.icon_marker_red
            ) as BitmapDrawable?
        val campsiteSymbol = PictureMarkerSymbol.createAsync(pinStarBlueDrawable).get()
        campsiteSymbol.loadAsync()
        val attributes = HashMap<String, Any>()
        val pointGraphic =
            Graphic(Point(x, y), attributes, campsiteSymbol)
        mGraphicsOverlay.graphics.add(pointGraphic)


    }

    override fun onResume() {
        mapView.resume()
        super.onResume()
    }

    override fun onPause() {
        mapView.pause()
        super.onPause()
    }

    override fun onDestroy() {
        mapView.dispose()
        super.onDestroy()
    }
}

三、编辑地图

基本操作

基本操作指的是地图中常用的操作,下面介绍其中八种:

  • 绘制点
  • 绘制直线(折线段)
  • 绘制曲线(直接根据手指轨迹进行绘制)
  • 绘制多边形
  • 绘制圆
  • 绘制图片marker
  • 绘制文字
  • 绘制自定义标注

1.绘制点

逻辑很简单,就是在地图上点击一下绘制一个点,因此需要对地图设置一个点击事件,但是地图不是普通的View,具体示例见代码:

mapView.onTouchListener = object : DefaultMapViewOnTouchListener(this,mapView){
            override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
                e?:super.onSingleTapConfirmed(e)
                if(drawType != -1){
                     e?:return false
                    val clickPoint = mMapView.screenToLocation( android.graphics.Point(e.x.roundToInt(),e.y.roundToInt()))
                    when(drawType){
                        0 ->  MapUtil.drawPoint(clickPoint)
                    }
                }

                return super.onSingleTapConfirmed(e)
            }
        }
        
// 地图工具类
object MapUtil {
    val mGraphicsOverlay = GraphicsOverlay()


    // 绘制点
    fun drawPoint(p: Point) {
        //SimpleMarkerSymbol.Style有如下六个值,分别代表不同形状
        // SimpleMarkerSymbol.Style.CIRCLE 圆
        // SimpleMarkerSymbol.Style.CROSS  十字符号
        // SimpleMarkerSymbol.Style.DIAMOND 钻石
        // SimpleMarkerSymbol.Style.SQUARE 方形
        // SimpleMarkerSymbol.Style.TRIANGLE 三角形
        // SimpleMarkerSymbol.Style.X       X形状
        val simpleMarkerSymbol = SimpleMarkerSymbol(SimpleMarkerSymbol.Style.CIRCLE, Color.RED, 20f)
        val graphic = Graphic(p, simpleMarkerSymbol)
        //清除上一个点
        mGraphicsOverlay.graphics.clear()
        mGraphicsOverlay.graphics.add(graphic)
    }
}

2.绘制直线

绘制直线和绘制点的逻辑类似,只是将点串成一条线 ,代码如图:

 when(drawType){
        0 ->  MapUtil.drawPoint(clickPoint)
        1 -> MapUtil.drawLine(clickPoint)
    
    }
    
// 绘制直线
fun drawLine(p: Point) {
    // 保存点
    mPointCollection.add(p)
    val polyline = Polyline(mPointCollection)

    //点 可不绘制
    drawPoint(p)

    //线
    //SimpleLineSymbol.Style 线段形状
    // SimpleLineSymbol.Style.DASH  - - - -
    // SimpleLineSymbol.Style.DASH_DOT--·--·--
    // SimpleLineSymbol.Style.DASH_DOT_DOT --··--·----··--·--
    // SimpleLineSymbol.Style.DOT ..........
    // SimpleLineSymbol.Style.NULL 不显示
    // SimpleLineSymbol.Style.SOLID  直线
    val simpleLineSymbol = SimpleLineSymbol(SimpleLineSymbol.Style.SOLID , Color.BLUE, 3f);
    val graphic = Graphic(polyline, simpleLineSymbol)
    mGraphicsOverlay.graphics.add(graphic)
}

3.绘制曲线

绘制曲线和绘制直线的逻辑完全一致,不一致的地方在于点更密集。既然需要采集比绘制直线更多的点,那么直线这个收集点的方法肯定不行了,但幸运的是这个DefaultMapViewOnTouchListener还提供了一个onScroll方法,就是地图滚动事件的回调方法,我们看看源码:

mapView.onTouchListener = object : DefaultMapViewOnTouchListener(this,mapView){
            override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
                e?:super.onSingleTapConfirmed(e)
                if(drawType != -1){
                    e?:return false
                    val clickPoint = mMapView.screenToLocation( android.graphics.Point(e.x.roundToInt(),e.y.roundToInt()))
                    when(drawType){
                        0 ->  MapUtil.drawPoint(clickPoint)
                        1 -> MapUtil.drawLine(clickPoint)
                    }
                }

                return super.onSingleTapConfirmed(e)
            }

            override fun onScroll(
                e1: MotionEvent?,
                e2: MotionEvent?,
                distanceX: Float,
                distanceY: Float
            ): Boolean {
                if(drawType == 2){
                    e1?:return false
                    e2?:return false
                    val p1 = mMapView.screenToLocation( android.graphics.Point(e1.x.roundToInt(),e1.y.roundToInt()))
                    val p2 = mMapView.screenToLocation( android.graphics.Point(e2.x.roundToInt(),e2.y.roundToInt()))
                    MapUtil.drawCurves(p1, p2)
                    // 返回true 地图将不在滑动的时候滚动
                    return true
                }
                return super.onScroll(e1, e2, distanceX, distanceY)
            }
        }
// 地图工具类
object MapUtil {
    // 绘制图层
    val mGraphicsOverlay = GraphicsOverlay()
    // 点集合
    private val mPointCollection = PointCollection(SpatialReferences.getWebMercator())

     // 绘制曲线
    fun drawCurves(p1: Point, p2: Point) {
        mPointCollection.add(p1)
        mPointCollection.add(p2)
        val polyline = Polyline(mPointCollection)
        val simpleLineSymbol = SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, Color.BLUE, 3f);
        val graphic = Graphic(polyline, simpleLineSymbol)
        mGraphicsOverlay.graphics.add(graphic)
    }
}

从代码可以看出,我们是将点采集更多了,然后将所有的点还是使用普通的方式绘制的直线,接下来看看效果:

4.绘制多边形

绘制多边形其实就是将线条合成一个面,但是面的合成也和线条形状一样有多种,由于是面,此处通过图案来演示:

  • SimpleFillSymbol.Style.BACKWARD_DIAGONAL
  • SimpleFillSymbol.Style.FORWARD_DIAGONAL
  • SimpleFillSymbol.Style.DIAGONAL_CROSS
  • SimpleFillSymbol.Style.HORIZONTAL

  • SimpleFillSymbol.Style.VERTICAL

  • SimpleFillSymbol.Style.CROSS
  • SimpleFillSymbol.Style.SOLID 颜色填充

  • SimpleFillSymbol.Style.NULL 无背景 知道了这几种背景填充,接下来看看多边形是如何实现绘制的:
when(drawType){
        0 ->  MapUtil.drawPoint(clickPoint)
        1 -> MapUtil.drawLine(clickPoint)
        3 -> MapUtil.drawPolygon(clickPoint)
    }
    
// 地图工具类
object MapUtil {
   

   // 绘制多边形
    fun drawPolygon(p: Point) {
        mGraphicsOverlay.graphics.clear()
        mPointCollection.add(p)
        // 一个点无法构成一个面
        if (mPointCollection.size == 1) {
            drawPoint(p)
            return
        }
        val polygon = Polygon(mPointCollection)
        val lineSymbol = SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, Color.GREEN, 3.0f)
        
        val simpleFillSymbol =
            SimpleFillSymbol(SimpleFillSymbol.Style.SOLID, Color.YELLOW, lineSymbol)
        val graphic = Graphic(polygon, simpleFillSymbol)
        mGraphicsOverlay.graphics.add(graphic)
    }
}

效果预览:

5.绘制圆

其实绘制圆和绘制多边形类型,不同的是绘制圆是由圆心和半径来确认圆的位置的。而我们用圆规画圆的时候,通过两个针尖所在的点就可以确认一个圆的位置,此处也是类似的。第一个点确定圆心的位置,第二个点确定半径,根据这个推理,我们可得到下面的代码:

when(drawType){
        0 ->  MapUtil.drawPoint(clickPoint)
        1 -> MapUtil.drawLine(clickPoint)
        3 -> MapUtil.drawPolygon(clickPoint)
        4 -> MapUtil.drawCircle(clickPoint)
    }
    
// 地图工具类
object MapUtil {
   

       // 绘制圆
    fun drawCircle(p: Point) {
        mGraphicsOverlay.graphics.clear()
        if (mPointCollection.size == 50) mPointCollection.clear()
        mPointCollection.add(p)
        // 只能确定圆心
        if (mPointCollection.size == 1) {
            drawPoint(p)
            return
        }
        // 根据勾三股四玄五的三角函数得到两个点之间的距离作为半径
        val x = mPointCollection[0].x - mPointCollection[1].x
        val y = mPointCollection[0].y - mPointCollection[1].y
        val radius = sqrt(x.pow(2.0) + y.pow(2.0))
        
        val center = mPointCollection[0]
        mPointCollection.clear()
        // 根据圆心和半径获取圆周的点
        for (point in getPoints(center, radius)) {
            mPointCollection.add(point)
        }
        val polygon = Polygon(mPointCollection)
        // 边线
        val lineSymbol = SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, Color.GREEN, 3.0f)
        // 填充风格 填充颜色 填充边框
        val simpleFillSymbol =
            SimpleFillSymbol(SimpleFillSymbol.Style.SOLID, Color.YELLOW, lineSymbol)
        val graphic = Graphic(polygon, simpleFillSymbol)
        mGraphicsOverlay.graphics.add(graphic)
    }

    /**
     * 通过中心点和半径计算得出圆形的边线点集合
     *
     * @param center
     * @param radius
     * @return
     */
    private fun getPoints(center: Point, radius: Double): Array<Point?> {
        val points = arrayOfNulls<Point>(50)
        var sin: Double
        var cos: Double
        var x: Double
        var y: Double
        for (i in 0..49) {
            sin = kotlin.math.sin(Math.PI * 2.0 * i / 50)
            cos = kotlin.math.cos(Math.PI * 2.0 * i / 50)
            x = center.x + radius * sin
            y = center.y + radius * cos
            points[i] = Point(x, y)
        }
        return points
    }
}

绘制圆在计算点以后,绘制圆和具体APi和普通的多边形都是一样的,接下来预览一下效果:

6.绘制图片marker

绘制图片marker类似于给地图固定点添加一个图标,实现也很简单:

when(drawType){
        0 ->  MapUtil.drawPoint(clickPoint)
        1 -> MapUtil.drawLine(clickPoint)
        3 -> MapUtil.drawPolygon(clickPoint)
        4 -> MapUtil.drawCircle(clickPoint)
        5 -> MapUtil.drawMarker(clickPoint,this@MainActivity)
    }
    
// 地图工具类
object MapUtil {
   

// 绘制图片marker
    fun drawMarker(p: Point, context: Context) {
        // 获取 drawable 资源
        val pinStarBlueDrawable =
            ContextCompat.getDrawable(context, R.mipmap.icon_marker_red) as BitmapDrawable?
        // 生成图片标记符号
        // val campsiteSymbol = PictureMarkerSymbol("图片网络地址")
        val campsiteSymbol = PictureMarkerSymbol.createAsync(pinStarBlueDrawable).get()
        // 异步加载
        campsiteSymbol.loadAsync()
        val attributes = HashMap<String, Any>()
        // 生成图画内容
        val pointGraphic =
            Graphic(p, attributes, campsiteSymbol)
        // 添加到图层
        mGraphicsOverlay.graphics.add(pointGraphic)
    }
}

6.绘制文本标记

绘制文本标记和绘制图片标记一样很简答,直接看代码吧:

when(drawType){
        0 ->  MapUtil.drawPoint(clickPoint)
        1 -> MapUtil.drawLine(clickPoint)
        3 -> MapUtil.drawPolygon(clickPoint)
        4 -> MapUtil.drawCircle(clickPoint)
        5 -> MapUtil.drawMarker(clickPoint,this@MainActivity)
        6 -> MapUtil.drawText(clickPoint)
    }
    
// 地图工具类
object MapUtil {
   

    // 绘制文字
    fun drawText(p: Point) {
        // 水平方向有左 中 右
        // 水平左 TextSymbol.HorizontalAlignment.LEFT 
        // 水平中 TextSymbol.HorizontalAlignment.CENTER 
        // 水平右 TextSymbol.HorizontalAlignment.RIGHT
        // 垂直方向支持上中下
        // 垂直上 TextSymbol.VerticalAlignment.TOP
        // 垂直中 TextSymbol.VerticalAlignment.MIDDLE
        // 垂直下 TextSymbol.VerticalAlignment.BOTTOM
        val textSymbol = TextSymbol(
            20f, "标记文字", Color.RED,
            TextSymbol.HorizontalAlignment.CENTER, TextSymbol.VerticalAlignment.MIDDLE
        )
        // 生成绘画内容
        val graphic = Graphic(p, textSymbol)
        // 清除之前的内容
        mGraphicsOverlay.graphics.clear()
        // 添加到图层
        mGraphicsOverlay.graphics.add(graphic)
    }
}

效果预览

7.绘制自定义标注

虽然上面可以支持添加图片marker,也支持文本内容,但是如果想要添加一个在图层上面包含图片和文本的标注应该怎么办呢?于是就有了下面的自定义标注。简单一句话概括,就是通过自定义View来显示标注信息,具体示例如下:

when(drawType){
        0 ->  MapUtil.drawPoint(clickPoint)
        1 -> MapUtil.drawLine(clickPoint)
        3 -> MapUtil.drawPolygon(clickPoint)
        4 -> MapUtil.drawCircle(clickPoint)
        5 -> MapUtil.drawMarker(clickPoint,this@MainActivity)
        6 -> MapUtil.drawText(clickPoint)
        7 -> MapUtil.drawCallout(clickPoint,mapView,this@MainActivity)
    }
    
// 地图工具类
object MapUtil {
   

    fun drawCallout(p: Point,mapView: MapView,context: Context) {
        val callout = mapView.callout
        if(callout.isShowing){
            callout.dismiss()
        }
        val view = LayoutInflater.from(context).inflate(R.layout.callout_delete_layout, null, false)
        view.setOnClickListener {
            callout.dismiss()
            EasyToast.DEFAULT.show("关闭标记")
        }
        callout.location = p
        callout.content = view
        callout.show()
    }
}

效果预览(注意尖角和圆角是地图设置的):

草图编辑器——SketchEditor

草图编辑器除了不支持绘制圆,其它图形都是没有问题的,而且使用方法也非常简单,直接看注释:

// 草图编辑器
private val mSketchEditor = SketchEditor()


实例化地图
... 
// 设置草图编辑器几何体的透明度
mSketchEditor.opacity = 0.5f
// 将草图编辑器添加到地图中
mapView.sketchEditor = mSketchEditor
val builder2 = XPopup.Builder(this).watchView(btn_sketch)
btn_sketch.setOnClickListener {
    builder2.asAttachList(
        arrayOf("单点", "多点", "折线", "多边形", "徒手画线", "徒手画多边形", "上一步", "下一步"), null
    ) { position, _ ->
        MapUtil.restDrawStatus()
        when (position) {
            0 -> mSketchEditor.start(SketchCreationMode.POINT)
            1 -> mSketchEditor.start(SketchCreationMode.MULTIPOINT)
            2 -> mSketchEditor.start(SketchCreationMode.POLYLINE)
            3 -> mSketchEditor.start(SketchCreationMode.POLYGON)
            4 -> mSketchEditor.start(SketchCreationMode.FREEHAND_LINE)
            5 -> mSketchEditor.start(SketchCreationMode.FREEHAND_POLYGON)
            6 -> if (mSketchEditor.canUndo()) mSketchEditor.undo()
            7 -> if (mSketchEditor.canRedo()) mSketchEditor.redo()

        }

    }
        .show()
}

btn_rest.setOnClickListener {
        // 第一种绘制的重置
        if (drawType != -1) {
            drawType = -1
            MapUtil.restDrawStatus()
        } else {
            // 是否绘制成功(有没有图形)
            if (!mSketchEditor.isSketchValid) {
                // 重置
                mSketchEditor.stop()
                return@setOnClickListener
            }
            // 从草图编辑器获得几何图形
            val sketchGeometry = mSketchEditor.geometry
            mSketchEditor.stop()
            if (sketchGeometry != null) {

                //从草图编辑器创建一个图形
                val graphic = Graphic(sketchGeometry)

                // 根据几何类型分配符号
                if (graphic.geometry.geometryType == GeometryType.POLYGON) {

                    val mLineSymbol =
                        SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, 0xFF8800, 4f)
                    val mFillSymbol =
                        SimpleFillSymbol(SimpleFillSymbol.Style.CROSS, 0x40FFA9A9, mLineSymbol)
                    graphic.symbol = mFillSymbol
                } else if (graphic.geometry.geometryType == GeometryType.POLYLINE) {
                    val mLineSymbol =
                        SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, 0xFF8800, 4f)
                    graphic.symbol = mLineSymbol
                } else if (graphic.geometry.geometryType == GeometryType.POINT ||
                    graphic.geometry.geometryType == GeometryType.MULTIPOINT
                ) {
                    val mPointSymbol = SimpleMarkerSymbol(
                        SimpleMarkerSymbol.Style.SQUARE,
                        0xFF0000, 20f
                    )
                    graphic.symbol = mPointSymbol
                }

                // 将图形添加到图形覆盖层
                mGraphicsOverlay.graphics.add(graphic)
            }
        }

    }

预览:

坐标计算

上面只是简单的绘制,但是对于开发来说这明显是不足的,因为我们没有得到绘制点的具体经纬度.对于经纬度,下面的解释比较贴切:

由于目前世界上只有美国才有全球定位系统(GPS),当我们实际做项目时,得到的坐标数据往往都是为GPS全球定位系统使用而建立的坐标系统,即我们所说的84坐标。而基于我国国情,这些真实坐标都是已经进行人为的加偏处理过后,才能进行出版和发布。所以,后台返回的是84坐标,想要在地图上显示正确的位置,就需要进行坐标转换。原文

上面第一种方法中,我们拿到的坐标是屏幕坐标android.graphics.Point,接下来转为了投影坐标(当然也支持地图坐标转为屏幕坐标的),这个时候这个坐标的经纬度值依然还不对,我们使用的时候还需要将投影坐标转为空间坐标,这里就需要用到本节的主角GeometryEngine了!

1.GeometryEngine坐标转换

  • 根据投影坐标获取默认地图坐标
Point wgsPoint = (Point) GeometryEngine.project(dp, mMapView.getSpatialReference(), null);
  • J02坐标转为G84坐标
val projectedPoint = GeometryEngine.project(clickPoint,SpatialReference.create(4236)) as Point
// SpatialReference.create(4236) = SpatialReferences.getWgs84()
  • G84坐标转为J02坐标

GCJ02:又称火星坐标系,是由中国国度测绘局制定的地舆坐标系统,是由WGS84加密后获得的坐标系。

由于GcJo2是一个加密后的结果,因此没有WSG84一样规范的WID,但是一样有办法实现。

package com.vincent.arcgisdemo.util

import com.esri.arcgisruntime.geometry.Point
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt

/**
 * <p>文件描述:坐标转换工具类<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/10/1 0001 <p>
 * <p>@update 2019/10/1 0001<p>
 * <p>版本号:1<p>
 *
 */
object TransformUtil {

    /**
     * 是否在国内
     */
    private fun outOfChina(lat: Double, lng: Double): Boolean {
        if (lng < 72.004 || lng > 137.8347) {
            return true
        }
        return lat < 0.8293 || lat > 55.8271
    }

    // 解密纬度
    private fun transformLat(x: Double, y: Double): Double {
        var ret =
            -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(abs(x))
        ret += (20.0 * sin(6.0 * x * Math.PI) + 20.0 * sin(2.0 * x * Math.PI)) * 2.0 / 3.0
        ret += (20.0 * sin(y * Math.PI) + 40.0 * sin(y / 3.0 * Math.PI)) * 2.0 / 3.0
        ret += (160.0 * sin(y / 12.0 * Math.PI) + 320 * sin(y * Math.PI / 30.0)) * 2.0 / 3.0
        return ret
    }
    // 解密经度
    private fun transformLon(x: Double, y: Double): Double {
        var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(abs(x))
        ret += (20.0 * sin(6.0 * x * Math.PI) + 20.0 * sin(2.0 * x * Math.PI)) * 2.0 / 3.0
        ret += (20.0 * sin(x * Math.PI) + 40.0 * sin(x / 3.0 * Math.PI)) * 2.0 / 3.0
        ret += (150.0 * sin(x / 12.0 * Math.PI) + 300.0 * sin(x / 30.0 * Math.PI)) * 2.0 / 3.0
        return ret
    }

    /**
     * 测算经纬度差值
     * @param lat 纬度
     * @param lng 经度
     * @return delta[0] 是纬度差,delta[1]是经度差
     */
    private fun delta(lat: Double, lng: Double): DoubleArray {
        val delta = DoubleArray(2)
        val a = 6378137.0
        val ee = 0.00669342162296594323
        val dLat = transformLat(lng - 105.0, lat - 35.0)
        val dLng = transformLon(lng - 105.0, lat - 35.0)
        val radLat = lat / 180.0 * Math.PI
        var magic = sin(radLat)
        magic = 1 - ee * magic * magic
        val sqrtMagic = sqrt(magic)
        delta[0] = dLat * 180.0 / (a * (1 - ee) / (magic * sqrtMagic) * Math.PI)
        delta[1] = dLng * 180.0 / (a / sqrtMagic * cos(radLat) * Math.PI)
        return delta
    }

    /**
     * WSG84 转 GCJ02 坐标
     * @param lat 纬度
     * @param lng 经度
     */
    fun  wsG84toGCJ02Point(latitude:Double,longitude:Double) : Point {
        if (outOfChina(latitude, longitude)) {
            return Point(latitude, longitude)
        }
        val delta = delta(latitude, longitude)
          return Point(latitude + delta[0], longitude + delta[1])
    }

    /**
     * GCJo2 转 WGS84 坐标
     * @param lat 纬度
     * @param lng 经度
     */
    fun  gcJo2toWGS84Point(latitude:Double,longitude:Double):Point {
        if (TransformUtil.outOfChina(latitude, longitude)) {
            return Point(latitude, longitude)
        }
        val delta = delta(latitude, longitude)
        return Point(latitude - delta[0], longitude - delta[1])
    }

}

计算长度

  • 计算给定折线的长度
GeometryEngine.length ( polyline:Polyline):Double
  • 计算几何的大地长度

注意LinearUnit——返回值的计量单位。如果为null,则默认为Id的线性单位METERS

GeometryEngine.lengthGeodetic ( geometry:Geometry,  lengthUnit:LinearUnit, curveType:GeodeticCurveType):Double

计算面积

GeometryEngine.area ( polygon:Polygon):Double

GeometryEngine.area ( envelope:Envelope):Double

GeometryEngine.areaGeodetic ( geometry:Geometry,  areaUnit:AreaUnit,  curveType:GeodeticCurveType):Double

计算给定几何的边界

GeometryEngine.boundary (geometry:Geometry):Geometry

合并两个给定几何的范围

GeometryEngine.combineExtents (geometry1:Geometry, geometry2:Geometry):Envelope 

合并几何集合的范围

GeometryEngine.contains ( container:Geometry, within:Geometry):Boolean

通过在几何图形中现有顶点之间绘制点来强化给定的几何图形。

GeometryEngine.densify ( geometry:Geometry,  maxSegmentLength:Double):Geometry

GeometryEngine.densifyGeodetic ( geometry:Geometry,  maxSegmentLength:Double,lengthUnit:LinearUnit,  curveType:GeodeticCurveType):Geometry

两个图形是否包含

后者是否包含前者

GeometryEngine.within ( within:Geometry,  container:Geometry):Boolean

四、常用接口介绍

在前面的实例中我们已经看到有通过点击事件获取点击点的经纬度,那么为什么我们点击事件的方法不是View.OnClickListener接口呢?接下来就给大家介绍一下包括DefaultMapViewOnTouchListener在内的接口吧!

  • MapScaleChangedListener

这个接口有一个回调方法mapScaleChanged,就是当地图的比例尺修改时调用,那么具体在什么时候使用这个接口呢?不知道是否还记得在初始化地图的时候,我们设置了地图的默认缩放级别是16,那么地图的最大与最小缩放分别是多少呢?网上有说是0~20,但是我没有找到相关文档,在初始化的过程中当中查看源码也没有发现检查最大值(有不能小于0的判断),因为最终实现方法是native方法。而且我们最后也没有发现获取缩放级别的相关Api,因此可以通过这个回调来处理比例尺与缩放级别的问题,从而实现限制缩放,防止地图无限放大或者缩小。

注意:mapScaleChanged方法被调用频率很高,且重置缩放的比例尺方法setViewpointScaleAsync是异步,这里的交互效果就会显得不那么理想。 解决方案最后在DefaultMapViewOnTouchListener内。

  • MapRotationChangedListener

这个接口的回调方法也只有一个,就是mapRotationChanged方法。当地图旋转的时候调用,也是一个被调用很高频的方法,如果有需要在地图旋转以后更新内容的需求,可以参考这个接口。

  • DefaultMapViewOnTouchListener DefaultMapViewOnTouchListener比较复杂,是一个实现类,大家可以先看看这张图:

当然,我们也没有必要完全去弄明白每一个方法的用途,只需要了解自己需要的方法即可。

单击 onSingleTapConfirmed 在项目通过这个方法实现了单击回调,有兴趣的同学也可以了解一下onSingleTapUp方法

长按 onLongPress 这个回调没有异议,项目中使用暂时也还没有发现问题

双击 onDoubleTouchDrag 这个方法名称有些歧义,因为这个方法只有双击回调,没有双指滑动的回调

开始缩放 onScaleEnd 上面说到的限制地图无限制缩放的时候,可以通过这个方法和onScaleBegin配合使用,在开始缩放的时候判断当前缩放的尺寸,如果已经不支持缩放直接消费掉此次事件即可

结束缩放 onScaleBegin 上面说到的限制地图无限制缩放的时候,可以通过这个方法和onScaleEnd配合使用,在开始缩放的时候判断当前缩放的尺寸,如果已经不支持缩放直接消费掉此次事件即可

手指滑动 onScroll 这个就是相当于默认滑动事件的MotionEvent.ACTION_MOVE,但是有一个奇特的地方在于当滑动结束的时候一定会回调onFling方法

滑动结束 onFling 当滑动结束的时候被调用,与View中的fling方法不一样

以上就是我在项目中使用到的关于地图的接口回调了。

五、天地地图接入

这个就比较简单了,去天地地图官网注册一个账号,然后申请一个应用,接着将得到的key放到工具类即可使用(目前发现未校验签名)。 下面的效果图使用的是天地地图的数据,没有使用Arcgis官网的地图数据了。

代码:

package com.vincent.arcgisdemo.util

import com.esri.arcgisruntime.arcgisservices.LevelOfDetail
import com.esri.arcgisruntime.arcgisservices.TileInfo
import com.esri.arcgisruntime.geometry.Envelope
import com.esri.arcgisruntime.geometry.Point
import com.esri.arcgisruntime.geometry.SpatialReference
import com.esri.arcgisruntime.layers.WebTiledLayer

object TianDiTuMethodsClass {
    val key = "58a2d8db46a9ea6d009a************"
    private val SubDomain = arrayOf("t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7")
    private val URL_VECTOR_2000 =
        "http://{subDomain}.tianditu.com/DataServer?T=vec_c&x={col}&y={row}&l={level}&tk=$key"
    private val URL_VECTOR_ANNOTATION_CHINESE_2000 =
        "http://{subDomain}.tianditu.com/DataServer?T=cva_c&x={col}&y={row}&l={level}&tk=$key"
    private val URL_VECTOR_ANNOTATION_ENGLISH_2000 =
        "http://{subDomain}.tianditu.com/DataServer?T=eva_c&x={col}&y={row}&l={level}&tk=$key"
    private val URL_IMAGE_2000 =
        "http://{subDomain}.tianditu.com/DataServer?T=img_c&x={col}&y={row}&l={level}&tk=$key"
    private val URL_IMAGE_ANNOTATION_CHINESE_2000 =
        "http://{subDomain}.tianditu.com/DataServer?T=cia_c&x={col}&y={row}&l={level}&tk=$key"
    private val URL_IMAGE_ANNOTATION_ENGLISH_2000 =
        "http://{subDomain}.tianditu.com/DataServer?T=eia_c&x={col}&y={row}&l={level}&tk=$key"
    private val URL_TERRAIN_2000 =
        "http://{subDomain}.tianditu.com/DataServer?T=ter_c&x={col}&y={row}&l={level}&tk=$key"
    private val URL_TERRAIN_ANNOTATION_CHINESE_2000 =
        "http://{subDomain}.tianditu.com/DataServer?T=cta_c&x={col}&y={row}&l={level}&tk=$key"

    private val URL_VECTOR_MERCATOR =
        "http://{subDomain}.tianditu.com/DataServer?T=vec_w&x={col}&y={row}&l={level}&tk=$key"
    private val URL_VECTOR_ANNOTATION_CHINESE_MERCATOR =
        "http://{subDomain}.tianditu.com/DataServer?T=cva_w&x={col}&y={row}&l={level}&tk=$key"
    private val URL_VECTOR_ANNOTATION_ENGLISH_MERCATOR =
        "http://{subDomain}.tianditu.com/DataServer?T=eva_w&x={col}&y={row}&l={level}&tk=$key"
    private val URL_IMAGE_MERCATOR =
        "http://{subDomain}.tianditu.com/DataServer?T=img_w&x={col}&y={row}&l={level}&tk=$key"
    private val URL_IMAGE_ANNOTATION_CHINESE_MERCATOR =
        "http://{subDomain}.tianditu.com/DataServer?T=cia_w&x={col}&y={row}&l={level}&tk=$key"
    private val URL_IMAGE_ANNOTATION_ENGLISH_MERCATOR =
        "http://{subDomain}.tianditu.com/DataServer?T=eia_w&x={col}&y={row}&l={level}&tk=$key"
    private val URL_TERRAIN_MERCATOR =
        "http://{subDomain}.tianditu.com/DataServer?T=ter_w&x={col}&y={row}&l={level}&tk=$key"
    private val URL_TERRAIN_ANNOTATION_CHINESE_MERCATOR =
        "http://{subDomain}.tianditu.com/DataServer?T=cta_w&x={col}&y={row}&l={level}&tk=$key"

    private val DPI = 96
    private val minZoomLevel = 1
    private val maxZoomLevel = 18
    private val tileWidth = 256
    private val tileHeight = 256
    private val LAYER_NAME_VECTOR = "vec"
    private val LAYER_NAME_VECTOR_ANNOTATION_CHINESE = "cva"
    private val LAYER_NAME_VECTOR_ANNOTATION_ENGLISH = "eva"
    private val LAYER_NAME_IMAGE = "img"
    private val LAYER_NAME_IMAGE_ANNOTATION_CHINESE = "cia"
    private val LAYER_NAME_IMAGE_ANNOTATION_ENGLISH = "eia"
    private val LAYER_NAME_TERRAIN = "ter"
    private val LAYER_NAME_TERRAIN_ANNOTATION_CHINESE = "cta"

    private val SRID_2000 = SpatialReference.create(4490)
    private val SRID_MERCATOR = SpatialReference.create(102100)
    private val X_MIN_2000 = -180.0
    private val Y_MIN_2000 = -90.0
    private val X_MAX_2000 = 180.0
    private val Y_MAX_2000 = 90.0

    private val X_MIN_MERCATOR = -20037508.3427892
    private val Y_MIN_MERCATOR = -20037508.3427892
    private val X_MAX_MERCATOR = 20037508.3427892
    private val Y_MAX_MERCATOR = 20037508.3427892
    private val ORIGIN_2000 = Point(-180.0, 90.0, SRID_2000)
    private val ORIGIN_MERCATOR = Point(-20037508.3427892, 20037508.3427892, SRID_MERCATOR)
    private val ENVELOPE_2000 = Envelope(X_MIN_2000, Y_MIN_2000, X_MAX_2000, Y_MAX_2000, SRID_2000)
    private val ENVELOPE_MERCATOR =
        Envelope(X_MIN_MERCATOR, Y_MIN_MERCATOR, X_MAX_MERCATOR, Y_MAX_MERCATOR, SRID_MERCATOR)

    private val SCALES = doubleArrayOf(
        2.958293554545656E8, 1.479146777272828E8,
        7.39573388636414E7, 3.69786694318207E7,
        1.848933471591035E7, 9244667.357955175,
        4622333.678977588, 2311166.839488794,
        1155583.419744397, 577791.7098721985,
        288895.85493609926, 144447.92746804963,
        72223.96373402482, 36111.98186701241,
        18055.990933506204, 9027.995466753102,
        4513.997733376551, 2256.998866688275,
        1128.4994333441375
    )
    private val RESOLUTIONS_MERCATOR = doubleArrayOf(
        78271.51696402048, 39135.75848201024,
        19567.87924100512, 9783.93962050256,
        4891.96981025128, 2445.98490512564,
        1222.99245256282, 611.49622628141,
        305.748113140705, 152.8740565703525,
        76.43702828517625, 38.21851414258813,
        19.109257071294063, 9.554628535647032,
        4.777314267823516, 2.388657133911758,
        1.194328566955879, 0.5971642834779395,
        0.298582141738970
    )

    private val RESOLUTIONS_2000 = doubleArrayOf(
        0.7031249999891485, 0.35156249999999994,
        0.17578124999999997, 0.08789062500000014,
        0.04394531250000007, 0.021972656250000007,
        0.01098632812500002, 0.00549316406250001,
        0.0027465820312500017, 0.0013732910156250009,
        0.000686645507812499, 0.0003433227539062495,
        0.00017166137695312503, 0.00008583068847656251,
        0.000042915344238281406, 0.000021457672119140645,
        0.000010728836059570307, 0.000005364418029785169
    )

    fun CreateTianDiTuTiledLayer(layerType: String): WebTiledLayer {
        return CreateTianDiTuTiledLayer(getTianDiTuLayerType(layerType));
    }

    fun CreateTianDiTuTiledLayer(layerType: LayerType): WebTiledLayer {
        var webTiledLayer: WebTiledLayer? = null
        var mainUrl = ""
        var mainName = ""
        var mainTileInfo: TileInfo? = null
        var mainEnvelope: Envelope? = null
        var mainIs2000 = false
        when (layerType) {
            LayerType.TIANDITU_VECTOR_2000 -> {
                mainUrl = URL_VECTOR_2000
                mainName = LAYER_NAME_VECTOR
                mainIs2000 = true
            }
            LayerType.TIANDITU_VECTOR_MERCATOR -> {
                mainUrl = URL_VECTOR_MERCATOR
                mainName = LAYER_NAME_VECTOR
            }
            LayerType.TIANDITU_IMAGE_2000 -> {
                mainUrl = URL_IMAGE_2000
                mainName = LAYER_NAME_IMAGE
                mainIs2000 = true
            }
            LayerType.TIANDITU_IMAGE_ANNOTATION_CHINESE_2000 -> {
                mainUrl = URL_IMAGE_ANNOTATION_CHINESE_2000
                mainName = LAYER_NAME_IMAGE_ANNOTATION_CHINESE
                mainIs2000 = true
            }
            LayerType.TIANDITU_IMAGE_ANNOTATION_ENGLISH_2000 -> {
                mainUrl = URL_IMAGE_ANNOTATION_ENGLISH_2000
                mainName = LAYER_NAME_IMAGE_ANNOTATION_ENGLISH
                mainIs2000 = true
            }
            LayerType.TIANDITU_IMAGE_ANNOTATION_CHINESE_MERCATOR -> {
                mainUrl = URL_IMAGE_ANNOTATION_CHINESE_MERCATOR;
                mainName = LAYER_NAME_IMAGE_ANNOTATION_CHINESE;
            }
            LayerType.TIANDITU_IMAGE_ANNOTATION_ENGLISH_MERCATOR -> {
                mainUrl = URL_IMAGE_ANNOTATION_ENGLISH_MERCATOR
                mainName = LAYER_NAME_IMAGE_ANNOTATION_ENGLISH
            }
            LayerType.TIANDITU_IMAGE_MERCATOR -> {
                mainUrl = URL_IMAGE_MERCATOR
                mainName = LAYER_NAME_IMAGE
            }
            LayerType.TIANDITU_VECTOR_ANNOTATION_CHINESE_2000 -> {
                mainUrl = URL_VECTOR_ANNOTATION_CHINESE_2000
                mainName = LAYER_NAME_VECTOR_ANNOTATION_CHINESE
                mainIs2000 = true
            }
            LayerType.TIANDITU_VECTOR_ANNOTATION_ENGLISH_2000 -> {
                mainUrl = URL_VECTOR_ANNOTATION_ENGLISH_2000
                mainName = LAYER_NAME_VECTOR_ANNOTATION_ENGLISH
                mainIs2000 = true
            }
            LayerType.TIANDITU_VECTOR_ANNOTATION_CHINESE_MERCATOR -> {
                mainUrl = URL_VECTOR_ANNOTATION_CHINESE_MERCATOR
                mainName = LAYER_NAME_VECTOR_ANNOTATION_CHINESE
            }
            LayerType.TIANDITU_VECTOR_ANNOTATION_ENGLISH_MERCATOR -> {
                mainUrl = URL_VECTOR_ANNOTATION_ENGLISH_MERCATOR
                mainName = LAYER_NAME_VECTOR_ANNOTATION_ENGLISH
            }
            LayerType.TIANDITU_TERRAIN_2000 -> {
                mainUrl = URL_TERRAIN_2000
                mainName = LAYER_NAME_TERRAIN
                mainIs2000 = true
            }
            LayerType.TIANDITU_TERRAIN_ANNOTATION_CHINESE_2000 -> {
                mainUrl = URL_TERRAIN_ANNOTATION_CHINESE_2000
                mainName = LAYER_NAME_TERRAIN_ANNOTATION_CHINESE
                mainIs2000 = true
            }
            LayerType.TIANDITU_TERRAIN_MERCATOR -> {
                mainUrl = URL_TERRAIN_MERCATOR
                mainName = LAYER_NAME_TERRAIN
            }
            LayerType.TIANDITU_TERRAIN_ANNOTATION_CHINESE_MERCATOR -> {
                mainUrl = URL_TERRAIN_ANNOTATION_CHINESE_MERCATOR
                mainName = LAYER_NAME_TERRAIN_ANNOTATION_CHINESE
            }
        }

        val mainLevelOfDetail = mutableListOf<LevelOfDetail>();
        var mainOrigin: Point? = null
        if (mainIs2000) {
            for (i in minZoomLevel..maxZoomLevel) {
                val item = LevelOfDetail(i, RESOLUTIONS_2000[i - 1], SCALES[i - 1])
                mainLevelOfDetail.add(item)
            }
            mainEnvelope = ENVELOPE_2000
            mainOrigin = ORIGIN_2000
        } else {
            for (i in minZoomLevel..maxZoomLevel) {
                val item = LevelOfDetail(i, RESOLUTIONS_MERCATOR[i - 1], SCALES[i - 1])
                mainLevelOfDetail.add(item);
            }
            mainEnvelope = ENVELOPE_MERCATOR;
            mainOrigin = ORIGIN_MERCATOR;
        }
        mainTileInfo = TileInfo(
            DPI,
            TileInfo.ImageFormat.PNG24,
            mainLevelOfDetail,
            mainOrigin,
            mainOrigin.getSpatialReference(),
            tileHeight,
            tileWidth
        )
        webTiledLayer = WebTiledLayer(
            mainUrl,
            SubDomain.toList(),
            mainTileInfo,
            mainEnvelope
        );
        webTiledLayer.setName(mainName)
        webTiledLayer.loadAsync()

        return webTiledLayer
    }

    fun getTianDiTuLayerType(layerType: String): LayerType {
        return when (layerType) {
            // 天地图矢量墨卡托投影地图服务
            "TIANDITU_VECTOR_MERCATOR" -> LayerType.TIANDITU_VECTOR_MERCATOR
            // 天地图矢量墨卡托中文标注
            "TIANDITU_VECTOR_ANNOTATION_CHINESE_MERCATOR" -> LayerType.TIANDITU_VECTOR_ANNOTATION_CHINESE_MERCATOR
            // 天地图矢量墨卡托英文标注
            "TIANDITU_VECTOR_ANNOTATION_ENGLISH_MERCATOR" -> LayerType.TIANDITU_VECTOR_ANNOTATION_ENGLISH_MERCATOR
            // 天地图影像墨卡托投影地图服务
            "TIANDITU_IMAGE_MERCATOR" -> LayerType.TIANDITU_IMAGE_MERCATOR
            // 天地图影像墨卡托投影中文标注
            "TIANDITU_IMAGE_ANNOTATION_CHINESE_MERCATOR" -> LayerType.TIANDITU_IMAGE_ANNOTATION_CHINESE_MERCATOR
            // 天地图影像墨卡托投影英文标注
            "TIANDITU_IMAGE_ANNOTATION_ENGLISH_MERCATOR" -> LayerType.TIANDITU_IMAGE_ANNOTATION_ENGLISH_MERCATOR
            // 天地图地形墨卡托投影地图服务
            "TIANDITU_TERRAIN_MERCATOR" -> LayerType.TIANDITU_TERRAIN_MERCATOR
            // 天地图地形墨卡托投影中文标注
            "TIANDITU_TERRAIN_ANNOTATION_CHINESE_MERCATOR" -> LayerType.TIANDITU_TERRAIN_ANNOTATION_CHINESE_MERCATOR
            // 天地图矢量国家2000坐标系地图服务
            "TIANDITU_VECTOR_2000" -> LayerType.TIANDITU_VECTOR_2000
            // 天地图矢量国家2000坐标系中文标注
            "TIANDITU_VECTOR_ANNOTATION_CHINESE_2000" -> LayerType.TIANDITU_VECTOR_ANNOTATION_CHINESE_2000
            // 天地图矢量国家2000坐标系英文标注
            "TIANDITU_VECTOR_ANNOTATION_ENGLISH_2000" -> LayerType.TIANDITU_VECTOR_ANNOTATION_ENGLISH_2000
            // 天地图影像国家2000坐标系地图服务
            "TIANDITU_IMAGE_2000" -> LayerType.TIANDITU_IMAGE_2000
            // 天地图影像国家2000坐标系中文标注
            "TIANDITU_IMAGE_ANNOTATION_CHINESE_2000" -> LayerType.TIANDITU_IMAGE_ANNOTATION_CHINESE_2000
            // 天地图影像国家2000坐标系英文标注
            "TIANDITU_IMAGE_ANNOTATION_ENGLISH_2000" -> LayerType.TIANDITU_IMAGE_ANNOTATION_ENGLISH_2000
            // 天地图地形国家2000坐标系地图服务
            "TIANDITU_TERRAIN_2000" -> LayerType.TIANDITU_TERRAIN_2000
            // 天地图地形国家2000坐标系中文标注
            "TIANDITU_TERRAIN_ANNOTATION_CHINESE_2000" -> LayerType.TIANDITU_TERRAIN_ANNOTATION_CHINESE_2000
            else -> LayerType.TIANDITU_VECTOR_2000
        }

    }
}

enum class LayerType {
    /**
     * 天地图矢量墨卡托投影地图服务
     */
    TIANDITU_VECTOR_MERCATOR,
    /**
     * 天地图矢量墨卡托中文标注
     */
    TIANDITU_VECTOR_ANNOTATION_CHINESE_MERCATOR,
    /**
     * 天地图矢量墨卡托英文标注
     */
    TIANDITU_VECTOR_ANNOTATION_ENGLISH_MERCATOR,
    /**
     * 天地图影像墨卡托投影地图服务
     */
    TIANDITU_IMAGE_MERCATOR,
    /**
     * 天地图影像墨卡托投影中文标注
     */
    TIANDITU_IMAGE_ANNOTATION_CHINESE_MERCATOR,
    /**
     * 天地图影像墨卡托投影英文标注
     */
    TIANDITU_IMAGE_ANNOTATION_ENGLISH_MERCATOR,
    /**
     * 天地图地形墨卡托投影地图服务
     */
    TIANDITU_TERRAIN_MERCATOR,
    /**
     * 天地图地形墨卡托投影中文标注
     */
    TIANDITU_TERRAIN_ANNOTATION_CHINESE_MERCATOR,
    /**
     * 天地图矢量国家2000坐标系地图服务
     */
    TIANDITU_VECTOR_2000,
    /**
     * 天地图矢量国家2000坐标系中文标注
     */
    TIANDITU_VECTOR_ANNOTATION_CHINESE_2000,
    /**
     * 天地图矢量国家2000坐标系英文标注
     */
    TIANDITU_VECTOR_ANNOTATION_ENGLISH_2000,
    /**
     * 天地图影像国家2000坐标系地图服务
     */
    TIANDITU_IMAGE_2000,
    /**
     * 天地图影像国家2000坐标系中文标注
     */
    TIANDITU_IMAGE_ANNOTATION_CHINESE_2000,
    /**
     * 天地图影像国家2000坐标系英文标注
     */
    TIANDITU_IMAGE_ANNOTATION_ENGLISH_2000,
    /**
     * 天地图地形国家2000坐标系地图服务
     */
    TIANDITU_TERRAIN_2000,
    /**
     * 天地图地形国家2000坐标系中文标注
     */
    TIANDITU_TERRAIN_ANNOTATION_CHINESE_2000

}


// 地图添加天地地图数据:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...

        //注意:在100.2.0之后要设置RequestConfiguration
        val requestConfiguration =  RequestConfiguration()
        requestConfiguration.getHeaders().put("referer", "http://www.arcgis.com");
        webTiledLayer.setRequestConfiguration(requestConfiguration)
        webTiledLayer1.setRequestConfiguration(requestConfiguration)
        webTiledLayer.loadAsync()
        webTiledLayer1.loadAsync()
        val basemap =  Basemap(webTiledLayer)
        basemap.getBaseLayers().add(webTiledLayer1)
        map.basemap = basemap
        mapView.map = map

        ...
    }

六、三维地图

SceneView示例

arcgisruntime是用了一个GeoView类作为地图的基类直接继承于ViewGroup,然后MapViewSceneView分别作为二维和三维地图的容器继承于GeoView。其实把SceneView当做MapView,把ArcGISScene当做ArcGISMap就行.

代码示例:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.SceneViewActivity">

    <com.esri.arcgisruntime.mapping.view.SceneView
        android:id="@+id/sceneview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </com.esri.arcgisruntime.mapping.view.SceneView>

</androidx.constraintlayout.widget.ConstraintLayout>

class SceneViewActivity : AppCompatActivity() {

    private val brest_buildings =
        " http://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Buildings_Brest/SceneServer"
    private val elevation_image_service =
        "http://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(com.vincent.arcgisdemo.R.layout.activity_scene_view)
        val arcGISScene = ArcGISScene()
        sceneview.scene = arcGISScene


    }


    override fun onResume() {
        super.onResume()
        sceneview.resume()
    }


    override fun onPause() {
        super.onPause()
        sceneview.pause()
    }

    override fun onDestroy() {
        super.onDestroy()
        sceneview.dispose()
    }
}

// 添加图层
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
         val arcGISTiledLayer =  ArcGISTiledLayer(
            "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer")
        arcGISScene.basemap = Basemap(arcGISTiledLayer)
        sceneview.scene = arcGISScene


    }
       

展示三维场景

三维通过接近真实世界的角度来可视化数据信息,三维场景的使用类似于MapViewArcGISMap,二维数据皆可加入三维场景,三维场景不同于二维,其具备高程表面(elevation surface)。

无高程表面(elevation surface

private val brest_buildings =
        " http://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Buildings_Brest/SceneServer"
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val arcGISScene = ArcGISScene()
        arcGISScene.basemap = Basemap.createImagery()
        sceneview.scene = arcGISScene
        val sceneLayer = ArcGISSceneLayer(brest_buildings)
        arcGISScene.operationalLayers.add(sceneLayer)
        // 设置三维场景视角镜头(camera)
        //latitude——纬度;可能是负的
        //
        //longitude——经度;可能是负的
        //
        //altitude-海拔;可能是负的
        //
        //heading——镜头水平朝向;可能是负的
        //
        //pitch——镜头垂直朝向;不一定是负的
        //
        //roll-转动的角度
        val camera =  Camera(48.378, -4.494, 200.0, 345.0, 65.0, 0.0)
        sceneview.setViewpointCamera(camera)
    }
  

使用高程表面(ArcGISTiledElevationSourceRasterElevationSource

ArcGISTiledElevationSource:将在线服务作为高程表面

RasterElevationSource:将本地DEM文件作为高程表面

private val elevation_image_service =
        "http://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer"
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val arcGISScene = ArcGISScene()
        arcGISScene.basemap = Basemap.createImagery()
        sceneview.scene = arcGISScene
       val elevationSource = ArcGISTiledElevationSource(elevation_image_service)
        arcGISScene.baseSurface.elevationSources.add(elevationSource)
        // 设置三维场景视角镜头(camera)
        //latitude——纬度;可能是负的
        //
        //longitude——经度;可能是负的
        //
        //altitude-海拔;可能是负的
        //
        //heading——镜头水平朝向;可能是负的
        //
        //pitch——镜头垂直朝向;不一定是负的
        //
        //roll-转动的角度
        val camera = Camera(28.4, 83.9, 10010.0, 10.0, 80.0, 0.0)
        sceneview.setViewpointCamera(camera)
    }

使用高层表面和不使用高层表面的体验来看,有点类似高度有没有变化一样的感觉,有兴趣的同学可以自己尝试一下。

其余部分可以直接参考文档:

七、热像图

官方提供效果图
根据官方示例展示的效果,其中有几个API需要我们掌握:

  • GeoprocessingJob 地理处理作业用于在服务上运行地理处理任务
  • GeoprocessingParameters 地理处理参数包含发送到目标地理处理任务的输入参数
  • GeoprocessingResult 从服务返回的输出参数
  • GeoprocessingTask 用于运行作为web服务发布的地理处理任务

具体可以通过代码来查看具体效果:

package com.vincent.arcgisdemo.ui

import android.app.DatePickerDialog
import android.app.Dialog
import android.app.ProgressDialog
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.esri.arcgisruntime.concurrent.Job
import com.esri.arcgisruntime.geometry.Point
import com.esri.arcgisruntime.geometry.SpatialReference
import com.esri.arcgisruntime.mapping.ArcGISMap
import com.esri.arcgisruntime.mapping.Basemap
import com.esri.arcgisruntime.mapping.Viewpoint
import com.esri.arcgisruntime.tasks.geoprocessing.GeoprocessingJob
import com.esri.arcgisruntime.tasks.geoprocessing.GeoprocessingString
import com.esri.arcgisruntime.tasks.geoprocessing.GeoprocessingTask
import com.haoge.easyandroid.easy.EasyToast
import com.vincent.arcgisdemo.R
import kotlinx.android.synthetic.main.activity_hot_spots.*
import kotlinx.android.synthetic.main.custom_alert_dialog.view.*
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*


class HotSpotsActivity : AppCompatActivity() {
    private val TAG = HotSpotsActivity::class.java.simpleName

    private lateinit var mGeoprocessingTask: GeoprocessingTask
    val hotspot_911_calls =
        "https://sampleserver6.arcgisonline.com/arcgis/rest/services/911CallsHotspot/GPServer/911%20Calls%20Hotspot"

    private lateinit var mMinDate: Date
    private lateinit var mMaxDate: Date
    private var canceled: Boolean = false
    private lateinit var mGeoprocessingJob: GeoprocessingJob
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_hot_spots)
        val map = ArcGISMap(Basemap.createTopographic())
        val center = Point(-13671170.0, 5693633.0, SpatialReference.create(3857))
        map.initialViewpoint = Viewpoint(center, 57779.0)
        mapView.map = map
        mGeoprocessingTask = GeoprocessingTask(hotspot_911_calls)
        mGeoprocessingTask.loadAsync()
        calendarButton.setOnClickListener {
            showDateRangeDialog()
        }
        showDateRangeDialog()
    }

    /**
     * 选择分析热点的时间区间
     */
    private fun showDateRangeDialog() {
        // create custom dialog
        val dialog = Dialog(this)
        val dialogView =
            LayoutInflater.from(this).inflate(R.layout.custom_alert_dialog, null, false)
        dialog.setContentView(dialogView)
        dialog.setCancelable(true)

        try {
            val mSimpleDateFormatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)

            // set default date range for the data set
            mMinDate = mSimpleDateFormatter.parse("1998-01-01")
            mMaxDate = mSimpleDateFormatter.parse("1998-05-31")
        } catch (e: ParseException) {
            Log.e(TAG, "Error in date format: " + e.message)
        }
        dialogView.fromDateText.setOnClickListener {
            showCalendar(InputCalendar.From, dialogView)
        }
        dialogView.toDateText.setOnClickListener {
            showCalendar(InputCalendar.To, dialogView)
        }

        // if button is clicked, close the custom dialog
        dialogView.analyzeButton.setOnClickListener {
            analyzeHotspots(
                dialogView.fromDateText.text.toString(),
                dialogView.toDateText.text.toString()
            )
            dialog.dismiss()

        }

        dialog.show()
    }

    /**
     * 显示日期选择器对话框,并将选择的日期写入正确的可编辑文本
     */
    private fun showCalendar(inputCalendar: InputCalendar, dialogView: View) {
        // create a date set listener
        val onDateSetListener =
            DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
                // build the correct date format for the query
                val date = StringBuilder()
                    .append(year)
                    .append("-")
                    .append(month + 1)
                    .append("-")
                    .append(dayOfMonth)
                // set the date to correct text view
                if (inputCalendar === InputCalendar.From) {
                    dialogView.fromDateText.setText(date)
                    try {
                        // limit the min date to after from date
                        val mSimpleDateFormatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
                        mMinDate = mSimpleDateFormatter.parse(date.toString())
                    } catch (e: ParseException) {
                        e.printStackTrace()
                    }

                } else if (inputCalendar === InputCalendar.To) {
                    dialogView.toDateText.setText(date)
                    try {
                        // limit the maximum date to before the to date
                        val mSimpleDateFormatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
                        mMaxDate = mSimpleDateFormatter.parse(date.toString())
                    } catch (e: ParseException) {
                        e.printStackTrace()
                    }

                }
            }

        // define the date picker dialog
        val calendar = Calendar.getInstance()
        val datePickerDialog = DatePickerDialog(
            this,
            onDateSetListener,
            calendar.get(Calendar.YEAR),
            calendar.get(Calendar.MONTH),
            calendar.get(Calendar.DAY_OF_MONTH)
        )
        datePickerDialog.datePicker.minDate = mMinDate.time
        datePickerDialog.datePicker.maxDate = mMaxDate.time
        if (inputCalendar === InputCalendar.From) {
            // start from calendar from min date
            datePickerDialog.updateDate(1998, 0, 1)
        }
        datePickerDialog.show()
    }

    /**
     * 运行地理处理作业,在加载时更新进度。工作完成后,将生成的ArcGISMapImageLayer加载到地图并重置MapView的视点。
     */
    private fun analyzeHotspots(from: String, to: String) {
        // 取消 mGeoprocessingJob 上一个请求
        mGeoprocessingJob.cancel()

        // 结果生成一个地图图像层。删除之前添加到地图的任何层
        mapView.map.operationalLayers.clear()

        // set canceled flag to false
        canceled = false

        // parameters
        val paramsFuture = mGeoprocessingTask.createDefaultParametersAsync()
        paramsFuture.addDoneListener {
            val geoprocessingParameters = paramsFuture.get()
            geoprocessingParameters.processSpatialReference = mapView.spatialReference
            geoprocessingParameters.outputSpatialReference = mapView.spatialReference

            val queryString = StringBuilder("(\"DATE\" > date '")
                .append(from)
                .append(" 00:00:00' AND \"DATE\" < date '")
                .append(to)
                .append(" 00:00:00')")

            geoprocessingParameters.inputs["Query"] = GeoprocessingString(queryString.toString())
            // create job
            mGeoprocessingJob = mGeoprocessingTask.createJob(geoprocessingParameters)

            // start job
            mGeoprocessingJob.start()

            // create a dialog to show progress of the geoprocessing job
            val progressDialog = ProgressDialog(this)
            progressDialog.setTitle("地理信息处理中")
            progressDialog.isIndeterminate = false
            progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
            progressDialog.max = 100
            progressDialog.setCancelable(false)
            progressDialog.setButton(
                DialogInterface.BUTTON_NEGATIVE, "Cancel"
            ) { dialog, _ ->
                dialog?.dismiss()
                // set canceled flag to true
                canceled = true
                mGeoprocessingJob.cancel()
            }
            progressDialog.show()
            // update progress
            mGeoprocessingJob.addProgressChangedListener {
                progressDialog.progress = mGeoprocessingJob.progress
            }
            mGeoprocessingJob.addJobDoneListener {
                progressDialog.dismiss()
                if (mGeoprocessingJob.status == Job.Status.SUCCEEDED) {
                    Log.i(TAG, "Job succeeded.")

                    val hotspotMapImageLayer = mGeoprocessingJob.result?.mapImageLayer

                    // add the new layer to the map
                    mapView.map.operationalLayers.add(hotspotMapImageLayer)
                    hotspotMapImageLayer?.addDoneLoadingListener {
                        // set the map viewpoint to the MapImageLayer, once loaded
                        mapView.setViewpointGeometryAsync(hotspotMapImageLayer.fullExtent)
                    }
                } else if (canceled) {
                    EasyToast.DEFAULT.show("Job canceled")
                    Log.i(TAG, "Job cancelled.")
                } else {
                    EasyToast.DEFAULT.show("Job did not succeed!")
                    Log.e(TAG, "Job did not succeed!")
                }
            }
        }
    }


    override fun onResume() {
        mapView.resume()
        super.onResume()
    }

    override fun onPause() {
        mapView.pause()
        super.onPause()
    }

    override fun onDestroy() {
        mapView.dispose()
        super.onDestroy()
    }
}

enum class InputCalendar {
    From,
    To
}

由于这部分内容没有在项目中实践过,只能简单的找到官方示例查看具体使用和相关源码注释,如果大家有这方面的使用心得,欢迎各位同学留言交流!


源码

传送门

参考:

官方文档

API介绍

arcgis-runtime-samples-android

ArcGIS for Android 100.3.0