Android高德猎鹰轨迹

先上一张效果图

一.在高德控制台创建应用获取key

这一步比较简单,没什么可说的。如何申请 Key

二.接入工程

1.通过Android Studio引入相关的包,官方文档说的比较明确,这里不再阐述,附上相关的链接:Android Studio 配置工程
2.配置清单文件,官网文档这里没有细说,具体可以查看示例代码中的配置,这里直接贴出相关配置。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          package="com.gfd.demo">
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <!--用于访问GPS定位-->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <!--用于获取运营商信息,用于支持提供运营商信息相关的接口-->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <!--用于访问wifi网络信息,wifi信息会用于进行网络定位-->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <!--用于获取wifi的获取权限,wifi信息会用来进行网络定位-->
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
    <!--用于访问网络,网络定位需要上网-->
    <uses-permission android:name="android.permission.INTERNET"/>
    <!--用于读取手机当前的状态-->
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <!--用于写入缓存数据到扩展存储卡-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <!--用于申请调用A-GPS模块-->
    <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"/>
    <!--用于申请获取蓝牙信息进行室内定位-->
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
   
    <application
        android:name=".app.AppApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:resizeableActivity="true"
        android:maxAspectRatio="2.4"
        android:theme="@style/AppTheme"
        tools:ignore="GoogleAppIndexingWarning">
      
        <!--高德地图配置-->
        <service android:name="com.amap.api.location.APSService"/>
        <service android:name="com.amap.api.track.AMapTrackService"/>

        <meta-data
            android:name="com.amap.api.v2.apikey"
            android:value="请输入您的用户Key"/>
      
    </application>
</manifest>

注意:Android6.0的动态权限申请。

三.获取Service ID

  • Service

即猎鹰轨迹服务。一个service对应一个轨迹管理系统,通过一个service可管理多个终端设备(即terminal),service的唯一标识符是sid。举例说明,针对某网约车公司的轨迹管理系统,可以创建一个service,在创建的service中管理所有营运车辆的轨迹。

  • Terminal

一个terminal代表现实中一个终端设备,它可以是个体、也可以是一辆车或者任何运动的物体。在同一个service中,terminaltid作为唯一标识。

要使用轨迹服务,必须先创建一个service,通过请求相应的接口来获取servie id

fun getTrackServiceId(name: String) {
     OkGo.post<String>("https://tsapi.amap.com/v1/track/service/add")
         .params("key", "2451a7584eb9aee041e0439bd3cdd51e")
         .params("name", name)
         .execute(object : StringCallback() {
             override fun onSuccess(response: Response<String>?) {
                 val json = response?.body().toString()
                 Logger.e("服务id:$json")
                 val data = Gson().fromJson(json, TrackServiceEntity::class.java)
                 AccountConfig.SERVICE_ID = data.data.sid.toLong()
             }
         })
}

有关接口的参数和返回值的详细说明见:接口说明

这里重点说一下参数key这个参数值。它是用户在高德地图官网申请Web服务API类型KEY,官网文档在这说的不是很详细。我们怎么获取这个key呢?在我们上面创建的应用下,去添加一个web类型key

输入自定义的名字后提交即可获取web类型的key:


这一步获取的key就是就是上面请求中key对应的值。

注意:service id 只需要获取一次,不需要每次使用的时候都去请求获取,除非添加新的service。(每个 Key 下最多注册15个 Service)

通过以上步骤,轨迹相关的配置已经完成,下面就具体看一下代码的实现。

四.相关代码

使用轨迹服务,首先需要初始化猎鹰sdk服务类以及一些参数的配置:

fun init(context: Context) {
    aMapTrackClient = AMapTrackClient(context)
    //将定位信息采集周期设置为2s,上报周期设置为20s,注意定位信息采集周期的范围应该是1s~60s,上报周期的范围是采集周期的5~50倍。
    aMapTrackClient.setInterval(2, 20)
    //设置轨迹点缓存大小为20M,最大为50M,默认值为50
    aMapTrackClient.setCacheSize(20)
    //设置定位模式,默认为高精度
    aMapTrackClient.setLocationMode(LocationMode.HIGHT_ACCURACY)
}

AMapTrackClient API文档

定位模式:
  • HIGHT_ACCURACY(高精度定位模式):

在这种定位模式下,将同时使用高德网络定位和卫星定位,优先返回精度高的定位

  • BATTERY_SAVING(低功耗定位模式):

在这种模式下,将只使用高德网络定位。使用网络定位就是使用基站定位。

  • DEVICE_SENSORS(仅设备定位模式):

在这种模式下,将只使用卫星定位。

其次需要创建用于接收猎鹰sdk服务启停状态的监听器:

//SimpleOnTrackLifecycleListener是实现接口OnTrackLifecycleListener的一个类,我们继承它只需要重写需要的方法即可。
onTrackLifecycleListener = object : SimpleOnTrackLifecycleListener() {

            override fun onBindServiceCallback(status: Int, msg: String?) {
                Logger.e("onBindServiceCallback, status: $status, msg: $msg")
            }
            
            //轨迹上报服务启动回调
            override fun onStartTrackCallback(status: Int, msg: String?) {
                if (status == ErrorCode.TrackListen.START_TRACK_SUCEE || status == ErrorCode.TrackListen.START_TRACK_SUCEE_NO_NETWORK) {
                    //启动动轨迹上报服务成功,开始轨迹采集
                    startGather()
                    isServiceRunning = true
                } else if (status == ErrorCode.TrackListen.START_TRACK_ALREADY_STARTED) {
                    //轨迹上报服务已经启动,重复启动
                    isServiceRunning = true
                } else {
                    //"error onStartTrackCallback, status: $status, msg: $msg"
                }
            }

            //轨迹上报服务停止回调
            override fun onStopTrackCallback(status: Int, msg: String?) {
                if (status == ErrorCode.TrackListen.STOP_TRACK_SUCCE) {
                    //停止轨迹上报服务成功
                    isServiceRunning = false
                    isGatherRunning = false
                } else {
                    //"error onStopTrackCallback, status: $status, msg: $msg"
                }
            }

            //定位采集开启回调
            override fun onStartGatherCallback(status: Int, msg: String?) {
                when (status) {
                    ErrorCode.TrackListen.START_GATHER_SUCEE -> {
                        //定位采集开启成功
                        isGatherRunning = true
                    }
                    ErrorCode.TrackListen.START_GATHER_ALREADY_STARTED -> {
                        //定位采集已经开启,重复启动
                        isGatherRunning = true
                    }
                    else -> {
                        //"error onStartGatherCallback, status: $status, msg: $msg"
                    }
                }
            }

            //停止定位采集回调
            override fun onStopGatherCallback(status: Int, msg: String?) {
                if (status == ErrorCode.TrackListen.STOP_GATHER_SUCCE) {
                    //定位采集停止成功,停止轨迹上报服务
                    stopTrack()
                    isGatherRunning = false
                } else {
                    //"error onStopGatherCallback, status: $status, msg: $msg"
                }
            }
        }

注意:要开启定位采集,需要首先启动轨迹上报服务,等服务启动成功后才能开启定位采集。

OnTrackLifecycleListener API文档

1.轨迹采集

轨迹采集需要提供服务id及终端id,服务id在上面通过请求相关接口已经获取到;终端id需要在该终端第一次使用轨迹采集的时候创建,所以,在使用轨迹采集的时候,需要先查询该终端id,如果没有创建过就创建一个新的,如果创建过就使用查询得到的终端id。

fun startTrack() {
    //查询终端id
    aMapTrackClient.queryTerminal(QueryTerminalRequest(AccountConfig.SERVICE_ID, AccountConfig.TERMINAL_NAME),
        object : SimpleOnTrackListener() {
            override fun onQueryTerminalCallback(queryTerminalResponse: QueryTerminalResponse?) {
                if (queryTerminalResponse?.isSuccess!!) {
                    if (queryTerminalResponse.isTerminalExist) {
                        // 当前终端已经创建过,直接使用查询到的terminal id
                        collectionTrack(queryTerminalResponse.tid)
                    } else {
                        // 当前终端是新终端,还未创建过,创建该终端并使用新生成的terminal id
                        aMapTrackClient.addTerminal(
                            AddTerminalRequest(
                                AccountConfig.TERMINAL_NAME,
                                AccountConfig.SERVICE_ID
                            ), object : SimpleOnTrackListener() {
                                override fun onCreateTerminalCallback(addTerminalResponse: AddTerminalResponse?) {
                                    if (addTerminalResponse?.isSuccess!!) {
                                        collectionTrack(addTerminalResponse.tid)
                                    }
                                }
                            })
                    }
                } else {
                    //"网络请求失败," + queryTerminalResponse.errorMsg
                }
            }
        })
}

获取对应的终端id后,开始轨迹收集:

private fun collectionTrack(terminalId: Long) {
    this.terminalId = terminalId
    if (uploadToTrack) {
        newTrack()
    } else {
        appendTrack()
    }
}
//不创建新的轨迹
private fun appendTrack() {
    // 不指定track id,上报的轨迹点是该终端的散点轨迹
    val trackParam = TrackParam(AccountConfig.SERVICE_ID, terminalId)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        trackParam.notification = createNotification()
    }
    aMapTrackClient.startTrack(trackParam, onTrackLifecycleListener)
}
//创建一个新的轨迹
private fun newTrack() {
    aMapTrackClient.addTrack(AddTrackRequest(AccountConfig.SERVICE_ID, terminalId),
        object : SimpleOnTrackListener() {
            override fun onAddTrackCallback(addTrackResponse: AddTrackResponse?) {
                if (addTrackResponse?.isSuccess!!) {
                    // trackId需要在启动服务后设置才能生效,因此这里不设置,而是在startGather之前设置了track id
                    trackId = addTrackResponse.trid
                    currentTrackId = trackId
                    Logger.e("轨迹id = $trackId")
                    val trackParam = TrackParam(AccountConfig.SERVICE_ID, terminalId)
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        trackParam.notification = createNotification()
                    }
                    aMapTrackClient.startTrack(trackParam, onTrackLifecycleListener)
                } else {
                    //"网络请求失败," + addTrackResponse.errorMsg
                }
            }
        })
}

如果创建一个新的轨迹,那么每一个行程对应着一条轨迹。

TrackParam API文档
AddTrackRequest API文档

2.查询轨迹

//查询轨迹,不包含散点轨迹
fun queryTrack(trackId:Long) {
    aMapTrackClient.queryTerminal(QueryTerminalRequest(AccountConfig.SERVICE_ID, AccountConfig.TERMINAL_NAME),
        object : SimpleOnTrackListener() {
            override fun onQueryTerminalCallback(queryTerminalResponse: QueryTerminalResponse?) {
                if (queryTerminalResponse?.isSuccess!!) {
                    if (queryTerminalResponse.isTerminalExist) {
                        val tid = queryTerminalResponse.tid
                        //设置轨迹查询的条件
                        val queryTrackRequest = QueryTrackRequest(
                            AccountConfig.SERVICE_ID,//轨迹服务id
                            tid, //终端id
                            trackId, // 轨迹id,不指定的话传入-1,查询所有轨迹,注意分页仅在查询特定轨迹id时生效,查询所有轨迹时无法对轨迹点进行分页
                            TimeUtils.getUnixTimeFromDate(track.startTime, TimeUtils.FORMAT_YMD_HMS),//开始时间
                            TimeUtils.getUnixTimeFromDate(track.endTime, TimeUtils.FORMAT_YMD_HMS),//结束时间
                            1,      // 启用去噪
                            if (isBindRoad) 1 else 0,// 是否绑路
                            0,      // 不进行精度过滤
                            DriveMode.DRIVING,  // 当前仅支持驾车模式
                            if (isRecoup) 1 else 0,     // 距离补偿
                            1000,   // 距离补偿,只有超过1km的点才启用距离补偿
                            1,  // 结果应该包含轨迹点信息
                            1,  // 返回第1页数据,如果未指定轨迹,分页将失效
                            100 // 一页不超过100条
                        )
                        aMapTrackClient.queryTerminalTrack(queryTrackRequest,
                            object : SimpleOnTrackListener() {
                                override fun onQueryTrackCallback(queryTrackResponse: QueryTrackResponse?) {
                                    if (queryTrackResponse?.isSuccess!!) {
                                        //如果指定了轨迹id,返回一条,如果不指定的话,返回指定时间段内的所有轨迹
                                        val tracks = queryTrackResponse.tracks
                                        if (tracks.isNotEmpty()) {
                                            var allEmpty = true
                                            for (track in tracks) {
                                                val points = track.points //轨迹点集合
                                                if (points != null && points.size > 0) {
                                                    allEmpty = false
                                                    //将轨迹绘制到地图上
                                                    drawTrackOnMap(points, textureMapView)
                                                }
                                            }
                                            if (allEmpty) run {
                                                //所有轨迹都无轨迹点,请尝试放宽过滤限制,如:关闭绑路模式
                                            } else {
                                                val sb = StringBuilder()
                                                sb.append("共查询到").append(tracks.size).append("条轨迹,每条轨迹行驶距离分别为:")
                                                for (track in tracks) {
                                                    sb.append(track.distance).append("m,")
                                                }
                                                sb.deleteCharAt(sb.length - 1)
                                            }
                                        } else {
                                            //未获取到轨迹                                     
                                        }
                                    } else {
                                        //查询历史轨迹失败
                                    }
                                }
                            })
                    } else {
                        //Terminal不存在
                    }
                } else {
                    //"网络请求失败,错误原因: " + queryTerminalResponse.errorMsg
                }
            }
        })
}

注意:查询轨迹设置的时间段不能超过24小时,如果超过24小时,直接查询失败。

QueryTrackRequest API文档
QueryTerminalResponse API文档

3.绘制轨迹

通过轨迹id查询获取到该轨迹对应的所有轨迹点,可以将这些轨迹点绘制到地图上,行成一条轨迹,为了显示的轨迹更加的平滑,需要对这些轨迹点进行平滑处理(对真实轨迹进行处理,实现去噪、平滑和抽稀):官方示例

/**
 * 在地图上绘制轨迹绘制到地图上
 * @param points List<Point>:轨迹点集合
 * @param textureMapView:地图View
 */
private fun drawTrackOnMap(points: List<Point>, textureMapView: TextureMapView) {
    //轨迹平滑处理
    val pathoptimizeList = trackSmoothHandle(points)
    val boundsBuilder = LatLngBounds.Builder()
    val polylineOptions = PolylineOptions()
    //设置轨迹的颜色
    polylineOptions.color(mContext.resources.getColor(R.color.baseColorBlue)).width(20f)
    var endIndex = pathoptimizeList.size - 1
    if (pathoptimizeList.isNotEmpty()) {
        // 起点
        val latLng = pathoptimizeList[0]
        val p = Point()
        val markerOptions = MarkerOptions()
                .position(latLng)
                .title("起点") //弹窗标题
                .infoWindowEnable(true) //显示弹窗
                .snippet("点击起点图标弹窗显示的内容")
                .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN)) //起点图标
            endMarkers.add(textureMapView.map.addMarker(markerOptions))
    }
    if (pathoptimizeList.size > 1) {
        // 终点
        val latLng = pathoptimizeList[endIndex]
        val p = Point()
        p.lat = latLng.latitude
        p.lng = latLng.longitude
        val markerOptions = MarkerOptions()
                .position(latLng)
                .title("终点")
                .infoWindowEnable(true)
                .snippet(snippet)
                //.icon(BitmapDescriptorFactory.fromBitmap(MarkerView(mContext).getBitmap())) 自定义图标
                .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE))
            endMarkers.add(textureMapView.map.addMarker(markerOptions))
    }
    for (latLng in pathoptimizeList.subList(0, endIndex + 1)) {
        polylineOptions.add(latLng)
        boundsBuilder.include(latLng)
    }
    val polyline = textureMapView.map.addPolyline(polylineOptions)
    polylines.add(polyline)
    val height = ScreenUtil.getScreenHeight(mContext)
    val width = ScreenUtil.getScreenWidth(mContext)
    //设置轨迹显示的区域  animateCamera方法:以动画方式按照传入的CameraUpdate参数更新地图状态,默认动画耗时250毫秒。
    textureMapView.map.animateCamera(
        CameraUpdateFactory.newLatLngBoundsRect(
            boundsBuilder.build(),
            ScreenUtil.dip2px(40f, mContext),
            ScreenUtil.dip2px(40f, mContext),
            height / 8,
            height / 5 * 3
        )
    )
}

/**
 * 轨迹平滑处理
 * @param points List<Point>:原轨迹点集合
 * @return List<LatLng>:平滑处理后的轨迹点集合
 */
fun trackSmoothHandle(points: List<Point>):List<LatLng>{
     val mpathSmoothTool = PathSmoothTool()
    //设置平滑处理的等级
    mpathSmoothTool.intensity = 4
    val mOriginList = ArrayList<LatLng>()
    points.forEach {
        mOriginList.add(LatLng(it.lat, it.lng))
    }
    return mpathSmoothTool.pathOptimize(mOriginList)
}

MarkerOptions API文档
轨迹点Point包含的
设置轨迹显示的区域API文档

实战

推荐阅读更多精彩内容

  • 去年有段时间得空,就把谷歌GAE的API权威指南看了一遍,收获颇丰,特别是在自己几乎独立开发了公司的云数据中心之后...
    骑单车的勋爵阅读 8,336评论 0 31
  • 写这篇文章的初衷是因为刚开始做猎鹰轨迹功能的时候,除了高德本身相关的API可参考,其他基本很少有相关的教程,也入了...
    宏炜阅读 1,191评论 14 4
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 3,637评论 0 12
  • 第一部分 创建爬虫 重点介绍网络数据采集的基本原理 : 如何用 Python 从网络服务器 请求信息,如何对服务器...
    万事皆成阅读 405评论 0 4
  • 天渐渐变亮,男人怒气冲冲的跺到厨房,看到女人的身影:“昨天交代你办孩子上学的事情还没办呢?”女人不言语,:“...
    雨落心飞阅读 203评论 4 6