开发者技术教程|基于百度地图SDK实现轨迹平滑移动

在LBS应用中,越来越多的业务场景都对连续性轨迹展示有更高标准的需求,诸如快递物流、生活服务、出行行业等。针对连续性轨迹展示的核心诉求是对真实轨迹场景的模拟,即要求连续性轨迹以平滑的线条展示,出行行业场景中还要考虑对真实路况的反映,同时,关注目标要实现沿路线平滑移动。基于百度地图SDK的可视化、计算等基础能力,可以完美的实现轨迹平滑移动。

 

实现原理

 

  • 多纹理(颜色)Polyline实现轨迹点路线+路况绘制;
  • Marker实现目标展示;
  • 路线划分为较小的Segment(以2m为标准),根据上一次目标位置点与当前目标位置点的绑路后坐标计算一个平滑周期内,目标需要移动的Segment数目,然后每次以1个Segment的为单位,计算目标的旋转角度,动态更新目标的坐标点和旋转角度,循环完成1个平滑周期内所有的Segment,实现平滑移动。

实现方法

 

下面以网约车行业的应用场景,分析详细的实现过程。 路线+路况   基于百度地图SDK的Polyline能力,可以实现路线绘制。使用多纹理虚线绘制Polyline的方式可以实现路况的模拟。具体代码如下: 1.路况纹理

1.  /** 

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

3.   * 

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

5.   * @return    填充纹理 

6.   */  

7.  public List<BitmapDescriptor> getTrafficTextureList() {  

8.      List<BitmapDescriptor> trafficTextureList = new ArrayList<>();  

9.      trafficTextureList.add(mUnknownTrafficTexture);  

10.     trafficTextureList.add(mSmoothTrafficTexture);  

11.     trafficTextureList.add(mSlowTrafficTexture);  

12.     trafficTextureList.add(mCongestionTrafficTexture);  

13.     trafficTextureList.add(mSevereCongestionTrafficTexture);  

14.   

15.     return trafficTextureList;  

16. }  

17. // 添加到纹理数组

18. if (mTrafficTextureList.isEmpty()) {  

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

20. } 

 

2.定义纹理索引

 

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

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

3.          continue;  

4.      }  

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

6.      int trafficStatus = mLinkPolyLineInfos.get(i).getTrafficStatus();  

7.      mTrafficTextureIndexList.add(trafficStatus);  

8.  } 

 

3. 绘制Polyline

 

1.  PolylineOptions polylineOptions = new PolylineOptions()  

2.      .points(mPolyLinePointList)  

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

4.      .width(routeLineWidth)  

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

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

7.      .zIndex(polylineZIndex);  

8.    

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

 

难点是路况的实现。关键点是纹理索引的计算。所谓纹理索引是用来标识Poyline中每个坐标点需要使用的纹理,索引的值是纹理数组中每个纹理的下标。如纹理索引数组为[0,0,1,1,2,3,4],表示Polyline第一个点使用纹理数组中第一个纹理,第二个点也使用第一个纹理,依次类推。 需要注意的是:纹理索引数组的长度如果大于纹理数组的长度或者纹理索引的值大于纹理数组的长度,则SDK会取纹理数组中最后一个纹理进行绘制。

 

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

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

2.  MarkerOptions carMarkerOptions = new MarkerOptions()  

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); 

小车平滑移动

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

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

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

 

为此百度地图SDK给出的解决方案如下:

1.  /** 

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

3.   */  

4.  private static final int MIN_SEGMENT_DISTANCE = 2;  

1.  double distance = 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 double getSectionRealDistance(LatLng startPoint, LatLng endPoint){  

11.     return DistanceUtil.getDistance(startPoint, endPoint);  

12. }  

其中,startPosition、endPosition是每段link的的起终点。路线划分的时候,需要同步处理路况数据。便于Polyline纹理索引的创建。 2.小车平滑距离计算 根据小车最新位置点以及上一次位置点绑路后的坐标点分别落在Segment数组索引计算出一个小车位置点获取频率周期内,需要平滑的Segment数量,从而得到小车平滑距离。 2.1 位置点绑路算法 使用叉积原则。如果叉积结果为0,则说明两条向量共线,并且如果位置点在以Segment起终点为对角顶点的矩形内,则说明位置点在Segment中。

1.  /** 

2.   * 计算叉积 

3.   */  

4.  private boolean cross(LatLng startPoint, LatLng endPoint, LatLng driverPosition) {  

5.      final double errorRange = 0.00001;  

6.    

7.      double startLatitude = startPoint.latitude;  

8.      double startLongitude = startPoint.longitude;  

9.      double endLatitude = endPoint.latitude;  

10.     double endLongitude = endPoint.longitude;  

11.     double driverLatitude = driverPosition.latitude;  

12.     double driverLongitude = driverPosition.longitude;  

13.   

14.     double result = Math.abs(  

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

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

17.     );  

18.   

19.     return  result < errorRange;  

20. }  

21.  

22. /** 

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

24.  * 

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

26.  */  

27. private boolean isInRange(LatLng startPoint, LatLng endPoint, LatLng driverPosition) {  

28.     final double errorRange = 0.00001;  

29.   

30.     double startLatitude = startPoint.latitude;  

31.     double startLongitude = startPoint.longitude;  

32.     double endLatitude = endPoint.latitude;  

33.     double endLongitude = endPoint.longitude;  

34.     double driverLatitude = driverPosition.latitude;  

35.     double driverLongitude = 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.平滑动画 以每个Segment为单位进行小车平滑。但是为了保证小车平滑效果,不能简单的从起点直接跳到终点,需要小车按照截距去做移动,从而以肉眼无法区分的跳动来实现平滑移动。有以下几个关键点需要处理: 3.1 斜率计算

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

1.  /** 

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

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

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

5.   * 

6.   * @param startPoint    小车当前起点 

7.   * @param endPoint      小车欲到达终点 

8.   */  

9.  private double getSlope(LatLng startPoint, LatLng endPoint) {  

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

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

12.         return Double.MAX_VALUE;  

13.     }  

14.   

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

16. }  

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

 

1.  /** 

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

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

4.   * 

5.   * @param  startPoint    小车当前起点 

6.   * @param  endPoint      小车欲到达终点 

7.   * @param  slope         斜率 

8.   */  

9.  private double getCarRotateAngle(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.     double deltaAngle = 0;  

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

30.         deltaAngle = 180;  

31.     }  

32.   

33.     double radio = Math.atan(slope);  

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

35. }  

36.  

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

38. mCarMarker.setRotate(carRouteAngle);  

 

3.3 平滑动画实现 以斜率不为0,沿纬度方向移动为例:

1.  rivate boolean movingAlongLatitude(LatLng startPoint, LatLng endPoint, double slope) {  

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

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

4.    

5.      /* 计算截距 */  

6.      double intercept = getInterception(slope, startPoint);  

7.    

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

9.      double sectionDistance = getSectionRealDistance(startPoint, endPoint);  

10.   

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

12.     double yMoveDistance = 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.         LatLng point;  

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

25.             point = new LatLng(i, startPoint.longitude);  

26.         } else {  

27.             point = new LatLng(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 double getInterception(double slope, LatLng point) {  

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

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

高级功能实现

路线擦除 在大多数场景,要求目标走过的路线擦除。路线擦除对整个技术方案要求较高。需要考虑已走过路线的计算和维护,目标状态的维护和异常保护。 可借鉴的实现方案是动态更新Polyline的坐标点和纹理索引数组来达到路线的擦除效果。通过记录目标(小车)已经走完的最新的Segment的索引,拿到终点坐标,再计算映射到路线Section的索引。然后对Polyline的坐标数组和纹理索引数组进行截取,以新的坐标数组和纹理索引数组进行绘制。

1.  try {  

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

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

4.      for (int i = 0; i < temp.size(); 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. }  

ETA信息展示

  除此之外,出行行业还有ETA信息展示的需求,可以使用InfoWindow或者Maker的方式进行信息展示。

 

1.  public void onRoutePlanInfoFreshFinished(float remainingDistance, long estimatedTime) {  

2.      Button info = new Button(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.  MarkerOptions infoWindowMarkerOptions = new MarkerOptions()  

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. }  

 

个性化地图

 

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

收藏 评论(0)
分享到: