综述
包含地图能力的应用(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。