基于百度地图SDK的轨迹平滑移动实现

综述

包含地图能力的应用(APP)中,连续性轨迹展示需求场景广泛。尤其是出行行业(网约车)、外卖行业等。

连续性轨迹展示的核心诉求是对真实轨迹场景的模拟,即要求连续性轨迹以平滑的线条展示,出行行业场景中还要考虑对真实路况的反映;同时,关注目标要实现沿路线平滑移动。

基于百度地图SDK的可视化、计算等基础能力,可以完美的实现轨迹平滑移动。

实现原理

多纹理(颜色)Polyline实现轨迹点路线+路况绘制;

Marker实现目标展示;

路线划分为较小的Segment(以2m为标准),根据上一次目标位置点与当前目标位置点的绑路后坐标计算一个平滑周期内,目标需要移动的Segment数目,然后每次以1个Segment的为单位,计算目标的旋转角度,动态更新目标的坐标点和旋转角度,循环完成1个平滑周期内所有的Segment,实现平滑移动。

下面以网约车行业的应用场景,分析详细的实现过程。

路线+路况

基于百度地图SDK的Polyline能力,可以实现路线绘制。使用多纹理虚线绘制Polyline的方式可以实现路况的模拟。具体代码如下:

基于百度地图SDK的Polyline能力,可以实现路线绘制。使用多纹理虚线绘制Polyline的方式可以实现路况的模拟。具体代码如下:

路况纹理

1. /** 

2.  * 获取路况填充纹理列表 

3.  * 

4.  * 路况类型:0:未知  1:畅通  2:缓行  3:拥堵  4:非常拥堵 

5.  * @return    填充纹理 

6.  */  

7. publicListgetTrafficTextureList() {

8.     List<BitmapDescriptor> trafficTextureList = newArrayList<>();

9. trafficTextureList.add(mUnknownTrafficTexture);

10. trafficTextureList.add(mSmoothTrafficTexture);

11. trafficTextureList.add(mSlowTrafficTexture);

12. trafficTextureList.add(mCongestionTrafficTexture);

13. trafficTextureList.add(mSevereCongestionTrafficTexture);

14.   

15.     returntrafficTextureList;

16. }  

17. // 添加到纹理数组

18. if(mTrafficTextureList.isEmpty()) {

19.        mTrafficTextureList.addAll(mDisplayOptions.getTrafficTextureList());  

20. } 

定义纹理索引

1. for (inti=0;i< length -1;i++) {

2.     if (null==mPolyLinePointList.get(i)) {

3.         continue;  

4.     }  

5.  // 数值对应路况状态,根据实际情况定义

6.     inttrafficStatus = mLinkPolyLineInfos.get(i).getTrafficStatus();

7.     mTrafficTextureIndexList.add(trafficStatus);

8. } 

绘制Polyline

1. PolylineOptionspolylineOptions=newPolylineOptions() .points(mPolyLinePointList)

3.                       .dottedLine(true) // 虚线绘制

4.                        .width(routeLineWidth)

5.                        .customTextureList(mTrafficTextureList)// 纹理数组,标识路况

6.                        .textureIndex(mTrafficTextureIndexList)// 纹理索引数组,标识每个点纹理

7.                        .zIndex(polylineZIndex);

8.   

9. mRoutePolyLine = (Polyline)mBaiduMap.addOverlay(polylineOptions);  

难点是路况的实现。关键点是纹理索引的计算。所谓纹理索引是用来标识Poyline中每个坐标点需要使用的纹理,索引的值是纹理数组中每个纹理的下标。如纹理索引数组为[0,0,1,1,2,3,4],表示Polyline第一个点使用纹理数组中第一个纹理,第二个点也使用第一个纹理,依次类推。

需要注意的是:纹理索引数组的长度如果大于纹理数组的长度或者纹理索引的值大于纹理数组的长度,则SDK会取纹理数组中最后一个纹理进行绘制。

目标展示

网约车行业可以以Marker的形式实现小车的展示。比较简单,代码如下:

1. /* 默认小车正北方向 */  

2. MarkerOptionscarMarkerOptions=newMarkerOptions()

3.                      .position(carPosition) // 坐标点

4.                       .icon(carIcon) // 车标

5.                        .flat(false) // 不平贴地图

6.                        .rotate(0.0f) // 车头正北方向

7.                        .zIndex(carMarkerZIndex)

8.                       .anchor(0.5f, 0.5f) // 锚点,相对路线居中

9.                       .perspective(false); // 关闭远大近小效果

10.   

11. mCarMarker= (Marker)mBaiduMap.addOverlay(carMarkerOptions);

小车平滑移动

3.3.1. 路线划分

3.3.1.1. 原因

A. 实际路线的单位是link,每个link的路况不是单一的,这导致Polyline绘制时路况无法模拟;

B. 每个link的长度不同,而且并非全部直线,存在曲线情况,这导致Polyline无法完美展示真实路线;

基于以上原因,考虑到当前地图支持最小展示比例尺为5m(非室内图场景),将每个link划分以2m为单位的Segment。既保证了两点之间都是直线,也确保了每个Segment都有独立的路况状态。

3.3.1.2. 方案

1. /** 

2.  * 每一段section的最小差分距离 

3.  */  

4. private static final int MIN_SEGMENT_DISTANCE = 2;  

1. doubledistance =getSectionRealDistance(startPosition, endPosition);

2. double differenceNumberEachSection = distance / MIN_SEGMENT_DISTANCE;  

3. if ((int)differenceNumberEachSection==0) {  

4.     return;  

5. }  

6.  

7. /** 

8.  * 根据起点和终点坐标计算Section的地理长度 

9.  */  

10. private doublegetSectionRealDistance(LatLng startPoint, LatLng endPoint){

11.     returnDistanceUtil.getDistance(startPoint,endPoint);

12. }  

其中,startPosition、endPosition是每段link的的起终点。

路线划分的时候,需要同步处理路况数据。便于Polyline纹理索引的创建。

3.3.2. 小车平滑距离计算

根据小车最新位置点以及上一次位置点绑路后的坐标点分别落在Segment数组索引计算出一个小车位置点获取频率周期内,需要平滑的Segment数量,从而得到小车平滑距离。

3.3.2.1. 位置点绑路算法

使用叉积原则。如果叉积结果为0,则说明两条向量共线,并且如果位置点在以Segment起终点为对角顶点的矩形内,则说明位置点在Segment中。

1. /** 

2.  * 计算叉积 

3.  */  

4. private booleancross(LatLng startPoint, LatLng endPoint, LatLng driverPosition) {

5.     final doubleerrorRange=0.00001;  

6.   

7.     doublestartLatitude=startPoint.latitude;

8.     doublestartLongitude=startPoint.longitude;

9.     doubleendLatitude=endPoint.latitude;

10.     doubleendLongitude=endPoint.longitude;

11.     doubledriverLatitude=driverPosition.latitude;

12.     doubledriverLongitude=driverPosition.longitude;

13.   

14.     doubleresult =Math.abs(

15.         (driverLatitude - startLatitude) * (endLongitude - startLongitude)  

16.             - (endLatitude - startLatitude) * (driverLongitude - startLongitude)  

17.     );  

18.   

19.     return  result

20. }  

21.  

22. /** 

23.  * 判断是否在segment起点,终点坐标组成的矩形范围内 

24.  * 

25.  * @return    true--在范围内;false--不在范围内 

26.  */  

27. private booleanisInRange(LatLng startPoint, LatLng endPoint, LatLng driverPosition) {

28.     final doubleerrorRange=0.00001;  

29.   

30.     doublestartLatitude=startPoint.latitude;

31.     doublestartLongitude=startPoint.longitude;

32.     doubleendLatitude=endPoint.latitude;

33.     doubleendLongitude=endPoint.longitude;

34.     doubledriverLatitude=driverPosition.latitude;

35.     doubledriverLongitude=driverPosition.longitude;

36.   

37.     if(Math.min(startLatitude, endLatitude) - errorRange <= driverLatitude) {

38.         if(driverLatitude <=Math.max(startLatitude, endLatitude) + errorRange) {

39.             if(Math.min(startLongitude, endLongitude) - errorRange <= driverLongitude) {

40.                 if(driverLongitude <=Math.max(startLongitude, endLongitude) + errorRange) {

41.                     return true;  

42.                 }  

43.             }  

44.         }  

45.     }  

46.   

47.     return false;  

48. }  

通过绑路后的坐标点跟Segment的起终点进行对比,确定位置点所在的Segment索引,做差得出一个周期内小车需要平滑移动的Segment数目。

3.3.3. 平滑动画

以每个Segment为单位进行小车平滑。但是为了保证小车平滑效果,不能简单的从起点直接跳到终点,需要小车按照截距去做移动,从而以肉眼无法区分的跳动来实现平滑移动。有有以下几个关键点需要处理:

3.3.3.1. 斜率计算

斜率有两个作用:计算小车的旋转角度(车头方向)和计算截距。斜率计算方法如下:

1. /** 

2.  * 计算两个坐标点之间的斜率 

3.  * 根据斜率来计算小车旋转角度和方向 

4.  * 以经度为X轴方向,纬度为Y轴方向 

5.  * 

6. * @paramstartPoint小车当前起点 

7. * @paramendPoint小车欲到达终点 

8.  */  

9. private doublegetSlope(LatLngstartPoint,LatLngendPoint) {

10.     /* 起点终点的经度相同,则认为斜率为Double.MAX_VALUE */  

11.     if(endPoint.longitude==startPoint.longitude) {

12.         returnDouble.MAX_VALUE;

13.     }  

14.   

15.     return (endPoint.latitude - startPoint.latitude) / (endPoint.longitude - startPoint.longitude);  

16. }  

3.3.3.2. 小车旋转角度(车头方向)计算

1. /** 

2.  * 根据两个坐标点,计算小车Marker的旋转角度 

3.  * Marker的角度是逆时针方向 

4.  * 

5. * @paramstartPoint小车当前起点 

6. * @paramendPoint小车欲到达终点 

7.  * @param  slope         斜率 

8.  */  

9. private doublegetCarRotateAngle(LatLng startPoint, LatLng endPoint,double slope) {  

10.     /* 经度相同 */  

11.     if(Double.MAX_VALUE== slope) {

12.         if(endPoint.latitude>startPoint.latitude) {

13.             return 360;  

14.         } else {  

15.             return 180;  

16.         }  

17.     }  

18.   

19.     /* 纬度相同 */  

20.     if (0 == slope) {  

21.         if(endPoint.longitude>startPoint.longitude) {

22.             return 270;  

23.         } else {  

24.             return 90;  

25.         }  

26.     }  

27.   

28.     doubledeltaAngle=0;  

29.     if ((endPoint.latitude - startPoint.latitude) * slope < 0) {  

30. deltaAngle=180;  

31.     }  

32.   

33.     doubleradio =Math.atan(slope);

34.     return ((180* (radio /Math.PI)) +deltaAngle) -90;  

35. }  

36.  

37. /* 设置小车旋转角度(车头方向) */  

38. mCarMarker.setRotate(carRouteAngle);  


3.3.3.3. 平滑动画实现

以斜率不为0,沿纬度方向移动为例

1. private booleanmovingAlongLatitude(LatLng startPoint, LatLng endPoint,double slope) {  

2.     /* 是否逆向行驶,true--逆向;false--正向行驶 */  

3.     boolean isReverse = (startPoint.latitude > endPoint.latitude);  

4.   

5.     /* 计算截距 */  

6.     doubleintercept =getInterception(slope,startPoint);

7.   

8.     /* 获取当前Section的长度 */  

9.     doublesectionDistance =getSectionRealDistance(startPoint, endPoint);

10.   

11.     /* 计算每次在Y轴(纬度)方向的移动距离,作为步长 */  

12.     doubleyMoveDistance=isReverse

13. ?getYMoveDistance(sectionDistance, slope)

14.         : (-1*getYMoveDistance(sectionDistance, slope));

15.   

16.   

17.     /* 平滑移动 */  

18.     for (double i = startPoint.latitude; i > endPoint.latitude == isReverse; i = i - yMoveDistance) {  

19.         if(mIsPauseRenderCarMoveThread) {

20.             return false;  

21.         }  

22.   

23. LatLngpoint;

24.         if(Double.MAX_VALUE== slope) {

25.             point = newLatLng(i,startPoint.longitude);

26.         } else {  

27.             point = newLatLng(i, ((i- intercept) / slope));

28.         }  

29.   

30.         /* 移动小车Marker位置,实现动画 */  

31.         setCarMarkerPoint(point);  

32.     }  

33.   

34.     return true;  

35. }  

其中,截距计算方法:

1. /** 

2.  * 根据点和斜率算取截距 

3.  * 以经度为X轴方向,纬度为Y轴方向 

4.  */  

5. private doublegetInterception(doubleslope,LatLngpoint) {

6.     returnpoint.latitude- slope *point.longitude;

7. }  

至此,基本的小车轨迹沿路线平滑移动功能实现。

4. 高级功能实现

4.1. 路线擦除

在大多数场景,要求目标走过的路线擦除。路线擦除对整个技术方案要求较高。需要考虑已走过路线的计算和维护,目标状态的维护和异常保护。

可借鉴的实现方案是动态更新Polyline的坐标点和纹理索引数组来达到路线的擦除效果。

通过记录目标(小车)已经走完的最新的Segment的索引,拿到终点坐标,再计算映射到路线Section的索引。然后对Polyline的坐标数组和纹理索引数组进行截取,以新的坐标数组和纹理索引素组进行绘制。

1. try {  

2.     List<Integer> temp = mTrafficTextureIndexList.subList(travelledPolyLineId, mTrafficTextureIndexList.size());  

3.     int[]tempIndex=new int[temp.size()];

4.     for (inti=0;i

5. tempIndex[i] =temp.get(i);

6.     }  

7.   

8.     if (null == mRoutePolyLine || mIsPauseRenderCarMoveThread) {  

9.         return;  

10.     }  

11.     mRoutePolyLine.setIndexs(tempIndex);  

12. } catch (Exception e) {  

13. e.printStackTrace();

14. }  

15.   

16. try {  

17.     List<LatLng> subPolyLinePointList = mPolyLinePointList.subList(travelledPolyLineId, mPolyLinePointList.size());  

18.   

19.     mRoutePolyLine.setPoints(subPolyLinePointList); 

20. } catch (Exception e) {  

21.     BDSDKLog.e(TAG_SYNC, "Get subList of PolyLinePointList failed", e);  

22. }  

4.2. ETA信息展示

除此之外,出行行业还有ETA信息展示的需求。

可以使用InfoWindow或者Maker的方式进行信息展示。

1. public voidonRoutePlanInfoFreshFinished(float remainingDistance, long estimatedTime) {  

2.     Button info = newButton(SynchronizationDemo.this);

3. info.setBackgroundResource(R.drawable.infowindow);

4. info.setWidth(300);  

5. info.setTextSize(10.0f);  

6. info.setText(String.format("剩余:%skm,预计:%dmin",remainingDistance/1000,estimatedTime/60));  

7.     mSyncDisplayManager.updateCarPositionInfoWindowView(info);  

8. }  

9. MarkerOptionsinfoWindowMarkerOptions=newMarkerOptions()

10. .position(carPosition)

11. .icon(BitmapDescriptorFactory.fromView(carInfoWindowView))

12. .zIndex(mCarInfoWindowZIndex)

13. .anchor(0.5f, 1.0f)  

14. .alpha(0.9f);  

15.   

16. if (null==mCarInfoWindow) {

17.     mCarInfoWindow = (Marker) mBaiduMap.addOverlay(infoWindowMarkerOptions);

18. } else {  

19. mCarInfoWindow.setPosition(carPosition);

20.     mCarInfoWindow.setIcon(BitmapDescriptorFactory.fromView(carInfoWindowView));  

21. }  

4.3. 个性化地图

为了突出业务数据和适配UI风格,应用(APP)可以使用个性化地图功能。具体的API可以参见http://lbsyun.baidu.com/index.php?title=androidsdk/guide/create-map/custommap

或者官网Demo。

整体效果展示

5. 写在最后

该文详细介绍了基于百度地图SDK能力实现轨迹平滑移动的关键技术点,实际处理的时候还是需要结合业务进行细致设计。比如,绘制和数据处理要分别起线程处理,防止主UI线程的阻塞。绘制和数据以生产者消费者的模式实现等等。

简单Demo,可参考百度地图开放平台官网Demo中TrackShowDemo.java。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容