ArcGIS Runtime Android 开发总结

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

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

1、Arcgis入门示例

标准地图

布局java

<?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>
复制代码

代码android

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()
            }
        }
复制代码

地图放大缩小

注意,此操做是异步。git

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

2、定位相关

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

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导航。web

  • NAVIGATION 车载导航

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

  • OFF

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

  • RECENTER

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

若是接下来须要获取当前定位点的经纬度信息,还要修改marker呢?接下来再看:app

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()
    }
}
复制代码

3、编辑地图

基本操做

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

  • 绘制点
  • 绘制直线(折线段)
  • 绘制曲线(直接根据手指轨迹进行绘制)
  • 绘制多边形
  • 绘制圆
  • 绘制图片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
复制代码

4、经常使用接口介绍

在前面的实例中咱们已经看到有经过点击事件获取点击点的经纬度,那么为何咱们点击事件的方法不是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方法不同

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

5、天地地图接入

这个就比较简单了,去天地地图官网注册一个帐号,而后申请一个应用,接着将获得的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

        ...
    }

复制代码

6、三维地图

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)
    }
复制代码

使用高层表面和不使用高层表面的体验来看,有点相似高度有没有变化同样的感受,有兴趣的同窗能够本身尝试一下。

其他部分能够直接参考文档:

7、热像图

官方提供效果图
根据官方 示例展现的效果,其中有几个 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

相关文章
相关标签/搜索