模仿百度地图的LBS服务——路线规划篇(v 3.1.1)

一、前言



转载请标明出处:http://blog.csdn.net/wlwlwlwl015/article/details/41091099

上一篇blog介绍了百度地图SDK的一些基本用法(模仿百度地图的LBS服务——基础地图篇),本篇blog将记录关于路径规划的具体使用方法,其中还包括了坐标转换地址、自定义RouteResult、计算路线的距离与所耗时间等等。下一篇博客将介绍离线地图的功能,同样也是高仿百度地图的离线地图模块(模仿百度地图的LBS服务——离线地图篇 Part1)



二、"到这去"功能剖析



这个是我集成的LBS服务中的核心功能,即我的当前定位点,到目标Marker怎么走的问题。首先简单的介绍一下百度SDK的路线规划,首先每一条路线都有三种规划方式,分别是:驾车、公交和步行。实现路线规划大体可以分为以下几个步骤:

a.设置起点和终点

b.通过RoutePlanSearch发起路线查询

c.在回调方法中根据得到RouteResult查询结果

d.自定义DrivingRouteOverlay类去做路线定制

e.将路线的Overlay绘制在地图上

f.提取RouteResult的路线中每一个Step的数据并做相应的展示以及总距离和总时间的计算


Part A.

根据之前的界面,我们再细化一下功能,点击“到这去”按钮的时候,首先从顶部弹出一个PopupWindow,有两个文本框和三个按钮,分别用于输入起点、终点,按钮分别是驾车、公交、步行这三种路线规划。但是很明显在“到这去”的功能中,起点和终点是不需要用户去输入的,起点就是当前所在的定位点,而终点就是我们所点击的Marker的位置,这个Marker在初始化的时候就已经知道了坐标,但是直接在文本框显示坐标是不行的,我们需要将坐标转换成位置信息再填充到PopupWindow的文本框中。首先我们看一下效果图,和上一篇博客的第三幅GIF图片一致:



可以看到点击“到这去”按钮,会在顶部弹出一个PopupWindow,里面有两个文本框,这两个文本框也就是起点和重点的位置信息。由于模拟器无法定位所以起点的信息没有显示出来,终点的信息则通过查询可以成功的显示出来。我们在添加Marker的时候就已经知道终点的坐标,所以在这里需要做的就是将坐标转换成位置信息再setText到文本框就OK了,这里通过坐标转换成位置信息就需要用到GeoCoder的反Geo搜索了(Geo是位置转坐标,反Geo是坐标转位置)。下面贴上代码,首先是初始化部分:

[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1. GeoCoder mCoder = null;  
  2. //初始化GeoCoder  
  3. mCoder = GeoCoder.newInstance();  
  4. mCoder.setOnGetGeoCodeResultListener(this);  


我们的Activity或Fragment需要实现OnGetGeoCoderResultListener接口,并重写这两个回调方法,这里我们只需要用法反Geo搜索,所以重写onGetReverseGeoCodeResult方法即可。

[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //Geo搜索  
  2.        @Override  
  3. public void onGetGeoCodeResult(GeoCodeResult arg0) {  
  4.     // TODO Auto-generated method stub  
  5. }  
  6.   
  7. // 反Geo搜索  
  8. @Override  
  9. public void onGetReverseGeoCodeResult(ReverseGeoCodeResult result) {  
  10.     // TODO Auto-generated method stub  
  11. }  
然后就可以在点击按钮的时候发起反地理编码请求(经纬度->地址信息)了:
[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1. // 到这去  
  2. class MyOnclickListenerTwo implements OnClickListener {  
  3.     @Override  
  4.     public void onClick(View v) {  
  5.         if (popupWindow != null && popupWindow.isShowing())  
  6.             popupWindow.dismiss();  
  7.         // 当前marker的坐标  
  8.         LatLng currentMarkerPosition = MyFragment.this.position;  
  9.         // 反Geo搜索  
  10.         mCoder.reverseGeoCode(new ReverseGeoCodeOption()  
  11.                 .location(currentMarkerPosition));  
  12.         dialog.show();  
  13.     }  
  14. }  


这里的MyFragment.this.position是我用来保存最后点击的特色数据坐标的全局变量,现在回头看一下OnMarkerClickListener中的代码(在上一篇blog)可以发现,每当触发Marker的点击事件时,都会将该Marker的position保存起来,而“到这去”的PopupWindow正是点击Marker之后才弹出的,所以通过这个变量自然可以得到当前Marker的坐标了。最后通过reverseGeoCode方法,根据ReverseGeoCodeOption传入的坐标,即可完成地址查询请求的发起。


当查询完毕后,我们就可以在回调方法中获取到位置信息了,由于reverseGeoCode发起的是异步请求,所以我们应当在回调方法中去得到结果并展示:

[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2. public void onGetReverseGeoCodeResult(ReverseGeoCodeResult result) {  
  3.     // TODO Auto-generated method stub  
  4.     if (result == null || result.error != SearchResult.ERRORNO.NO_ERROR) {  
  5.         Toast.makeText(getActivity(), "抱歉,未能找到结果", Toast.LENGTH_LONG)  
  6.                 .show();  
  7.         return;  
  8.     }  
  9.     reverseGeoCodeResult = result.getAddress();  
  10.     // 当前我的位置  
  11.     String address = mLocation.getAddrStr();  
  12.     dialog.dismiss();  
  13.     showPopupWindowTwo(address, reverseGeoCodeResult);  
  14. }  

Part B.

有了起点和终点,接下来我们就可以通过点击三种不同的按钮分别去查询路线信息了。在百度的SDK中,路线是由RouteLine这个类表示的,它有三个直接子类,分别是DrivingRouteLine(驾车路线)TransitRouteLine(公交路线),WalkingRouteLine(步行路线),而每一条路线又是由多个路段组成的,在百度的SDK中,路段是由RouteStep这个类表示的,同理它也有几个直接子类,分别是DrivingRouteLine.DrivingStepTransitRouteLine.TransitStepWalkingRouteLine.WalkingStep。我们通常在路线检索的结果(RouteResult)中可以得到RouteLine对象,进而通过它的getAllStep()方法得到所有的Step并遍历依次获取路段的详细数据。


下面贴代码,同上首先是初始化部分:

[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1. // 路线  
  2. RouteLine route = null;  
  3. // 路线搜索相关  
  4. RoutePlanSearch mSearch = null;  
  5. // 初始化路线查询对象  
  6. mSearch = RoutePlanSearch.newInstance();  
  7. // 设置路线查询结果监听  
  8. mSearch.setOnGetRoutePlanResultListener(this);  
我们的Activity或Fragment需要实现 OnGetRoutePlanResultListener接口,并重写三种路线查询结果的回调方法:

[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1.        @Override  
  2. public void onGetDrivingRouteResult(DrivingRouteResult arg0) {  
  3.     // TODO Auto-generated method stub  
  4. }  
  5.   
  6. @Override  
  7. public void onGetTransitRouteResult(TransitRouteResult arg0) {  
  8.     // TODO Auto-generated method stub    
  9. }  
  10.   
  11. @Override  
  12. public void onGetWalkingRouteResult(WalkingRouteResult arg0) {  
  13.     // TODO Auto-generated method stub    
  14. }  
然后就可以在点击按钮的时候发起路线查询的请求了:

[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1. // 发起路线规划搜索  
  2.     class MySearchButtonProcessClickListener implements OnClickListener {  
  3.         @Override  
  4.         public void onClick(View v) {  
  5.             if (popupWindowTwo != null && popupWindowTwo.isShowing()) {  
  6.                 popupWindowTwo.dismiss();  
  7.             }  
  8.               
  9.             // 重置浏览节点的路线数据  
  10.             if (route != null)  
  11.                 route = null;  
  12.             if (routeOverlay != null)  
  13.                 routeOverlay.removeFromMap();  
  14.             mBaiduMap.clear();  
  15.   
  16.             // 清空路线Result  
  17.             if (drResult != null)  
  18.                 drResult = null;  
  19.             if (trResult != null)  
  20.                 trResult = null;  
  21.             if (wrResult != null)  
  22.                 wrResult = null;  
  23.   
  24.             // 再标记一遍特色数据的marker  
  25.             addMarkers();  
  26.   
  27.             // 设置起终点信息,对于tranist search 来说,城市名无意义  
  28.             PlanNode stNode = null;  
  29.             PlanNode enNode = null;  
  30.             String cityName = mLocation.getCity();  
  31.             if (position != null) {  
  32.                 // 根据坐标查询 我的坐标 ---> 目标marker  
  33.                 stNode = PlanNode.withLocation(new LatLng(mLocation  
  34.                         .getLatitude(), mLocation.getLongitude()));  
  35.                 enNode = PlanNode.withLocation(position);  
  36.             } else {  
  37.                 // 根据城市名查询  
  38.                 stNode = PlanNode.withCityNameAndPlaceName(cityName, eStart  
  39.                         .getText().toString());  
  40.                 enNode = PlanNode.withCityNameAndPlaceName(cityName, eEnd  
  41.                         .getText().toString());  
  42.             }  
  43.             if (v.getId() == R.id.drive) {  
  44.                 // 显示Loading  
  45.                 dialog.show();  
  46.                 mSearch.drivingSearch((new DrivingRoutePlanOption()).from(  
  47.                         stNode).to(enNode));  
  48.             } else if (v.getId() == R.id.transit) {  
  49.                 // 显示Loading  
  50.                 dialog.show();  
  51.                 mSearch.transitSearch((new TransitRoutePlanOption())  
  52.                         .from(stNode).city(cityName).to(enNode));  
  53.             } else if (v == btnWalk) {  
  54.                 // 显示Loading  
  55.                 dialog.show();  
  56.                 mSearch.walkingSearch(new WalkingRoutePlanOption().from(stNode)  
  57.                         .to(enNode));  
  58.             }  
  59.   
  60.         }  
  61.   
  62.     }  

第10行到25行是一些 重置操作,毕竟每次绘制路线的时候都要清空关于上次绘制的信息,所以这里根据自己的情况去控制。可以看到33行和38行中我展示了两种设置起点终点的方法,分别是通过坐标查询和通过城市名查询,用任意一种都可以,官方提供了这两种选择。最后从43行开始,根据点击不同的按钮进入不同的分支,分别发起不同的路线查询。


Part C.

同Geo搜索类似,所有的搜索请求都是异步的,所以我们自然是在查询结果的回调方法中去获取结果数据了,由于这三种基本都是类似的所以我只写一下驾车的方案。驾车路线对应的回调方法是onGetDrivingRouteResult,在百度的SDK中查询出来的路线结果都是由SearchResult这个类表示的,那么自然的它也有以下的几个直接子类: DrivingRouteResult,TransitRouteResult,WalkingRouteResult,它们分别表示了这三种方案的查询结果。下面是c、d、e、f四个步骤的整体代码,先全部贴出来,再分段解释一下重点的部分:

[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1. // 驾车  
  2. @Override  
  3. public void onGetDrivingRouteResult(final DrivingRouteResult result) {  
  4.     // 关闭Loading  
  5.     dialog.dismiss();  
  6.     if (result == null || result.error != SearchResult.ERRORNO.NO_ERROR) {  
  7.         Toast.makeText(getActivity(), "抱歉,未找到结果", Toast.LENGTH_SHORT)  
  8.                 .show();  
  9.     }  
  10.     if (result.error == SearchResult.ERRORNO.AMBIGUOUS_ROURE_ADDR) {  
  11.   
  12.         return;  
  13.     }  
  14.     if (result.error == SearchResult.ERRORNO.NO_ERROR) {  
  15.   
  16.         if (stepDetails.size() != 0)  
  17.             stepDetails.clear();  
  18.         if (distance != 0)  
  19.             distance = 0;  
  20.         if (needTime != 0)  
  21.             needTime = 0;  
  22.   
  23.         route = result.getRouteLines().get(0);  
  24.         DrivingRouteOverlay overlay = new MyDrivingRouteOverlay(mBaiduMap);  
  25.         routeOverlay = overlay;  
  26.   
  27.         mBaiduMap.setOnMarkerClickListener(new MyMarkerClickListener());  
  28.   
  29.         overlay.setData(result.getRouteLines().get(0));  
  30.         overlay.addToMap(); // 将所有Overlay 添加到地图上  
  31.         overlay.zoomToSpan(); // 缩放地图,使所有Overlay都在合适的视野内 注:  
  32.                                 // 该方法只对Marker类型的overlay有效  
  33.         // 显示清理marker的垃圾桶按钮  
  34.         imageButtonClear.setVisibility(View.VISIBLE);  
  35.   
  36.         List<DrivingRouteLine> routeLines = result.getRouteLines();  
  37.         List<DrivingStep> steps = routeLines.get(0).getAllStep();  
  38.         // 分为N步  
  39.         for (int i = 0; i < steps.size(); i++) {  
  40.             String instructions = steps.get(i).getInstructions();  
  41.             int direction = steps.get(i).getDirection();  
  42.             int distance = steps.get(i).getDistance();  
  43.             this.distance += distance; // 叠加每一个step的distance  
  44.             String entraceInstructions = steps.get(i)  
  45.                     .getEntraceInstructions();  
  46.             String title = steps.get(i).getEntrace().getTitle();  
  47.             stepDetails.add((i + 1) + "." + instructions);  
  48.         }  
  49.   
  50.         needTime = distance / 550;  
  51.   
  52.         // 显示查看路线详情的按钮  
  53.         imageButtonShowDetail.setVisibility(View.VISIBLE);  
  54.         imageButtonShowDetail.setOnClickListener(new OnClickListener() {  
  55.             @Override  
  56.             public void onClick(View v) {  
  57.                 // TODO Auto-generated method stub  
  58.                 // Show路线详情的PopupWindow  
  59.                 showPopupWindowFour("驾车方案",  
  60.                         MapUtil.distanceFormatter(distance),  
  61.                         MapUtil.timeFormatter(needTime));  
  62.   
  63.             }  
  64.         });  
  65.     }  
  66.   
  67. }  

第16行到第21行依旧是在绘制路线之前做的清理工作,它们分别是路线详情、路线的总距离和需要花费的总时间,自然应到在每次绘制新路线之前重置。第23行通过DrivingRouteResult的getRouteLines()得到所有的规法方案(方案往往不止一种,比如:最短时间的方案、最短距离的方案、少走高速的方案等等),由于我们集成的服务没必要细化到这种程度,所以通过get(0)获取到默认的方案即可。

第24行是一个关键点,体现了Part D的内容。在百度地图SDK中,提供了一个用于显示和管理多个覆盖物的类——OverlayManager,同样的它的直接子类有DrivingRouteOverlayTransitRouteOverlay,WalkingRouteOverlay。所以我们如果要在地图是绘制出路线的Overlay,那么无疑是要通过OverlayManager的这些子类去实现的。可是我们在24行看到的是newMyDrivingRouteOverlay(mBaiduMap)而不是DrivingRouteOverlay,那么没错,这个类也是我们自己定制的。那么自定义的DrivingRouteOverlay究竟能做些什么呢,我们打开官方给出的API可以发现:

文档中写的很清楚重写这些方法分别能干什么了,那么我们根据需求去重写它们即可:


如果不需要上面的这些功能就直接去new一个DrivingRouteOverlay即可,下面贴出我的自定义类——MyDrivingRouteOverlay,很简单,只是修改了路线起点和终点的图标而已:

[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1. // 定制DrivingRouteOverly  
  2.     private class MyDrivingRouteOverlay extends DrivingRouteOverlay {  
  3.   
  4.         public MyDrivingRouteOverlay(BaiduMap baiduMap) {  
  5.             super(baiduMap);  
  6.         }  
  7.   
  8.         @Override  
  9.         public BitmapDescriptor getStartMarker() {  
  10.             if (useDefaultIcon) {  
  11.                 return BitmapDescriptorFactory.fromResource(R.drawable.icon_st);  
  12.             }  
  13.             return null;  
  14.         }  
  15.   
  16.         @Override  
  17.         public BitmapDescriptor getTerminalMarker() {  
  18.             if (useDefaultIcon) {  
  19.                 return BitmapDescriptorFactory.fromResource(R.drawable.icon_en);  
  20.             }  
  21.             return null;  
  22.         }  
  23.     }  

第29行到31行完成了在地图上绘制路线的Overlay,即Part E需要完成的工作。

在这里注意一下27行,我重新设置了一遍OnMarkerListener,这样做的原因就是绘制出来的路线上存在RouteNode,即路线上的节点,如果不重写DrivingRouteOverlay的onMarkerClick方法或者onRuteNodeClick方法,那么点击节点的时候就会触发BaiduMap的OnMarkerClickListener,这样节点的点击和我们大头针的点击事件就会冲突,很明显一般情况下都是需要区分的。所以有以下两种解决办法:

1.不重写任何方法,在BaiduMap的OnMarkerClickListener中做判断,根据RouteNode和我们的自定义Marker区分开处理即可(也就是我在上一篇blog中说到的路线节点,我是通过setTitle来区分的)。

2.重写DrivingRouteOverlay的onMarkerClick方法或onRuteNodeClick方法即可。


最后绘制完路线,我们需要做的就很明显了——查看详情。根据需求我们需要展示每一个Step的详细信息、整体Route的距离以及需要花费的时间。通过官方文档可以清楚的看到,DrivingRouteLine的父类RouteLine提供了一个方法来获取所有的路段——getAllStep(),返回该条路线的所有路段集合——List<DrivingStep>,如37行所示。最后遍历List<DrivingStep>,根据情况封装数据,最后在ListView中展示即可。当然除了每一个路段的详情,还需要计算总距离和总时间,这个也是模仿百度地图的详情页面做的,下面就简单的谈一下关于总距离和总时间的计算问题。


Part F.

距离计算

计算距离有两种方法,第一种是通过调用DrivingRouteLine的父类RouteLine提供的getDistance()方法即可得到一条路线的距离,返回值为int类型,单位是米。第二种方法是在循环中去叠加每一个Step的distance即可,需要注意的就是每次计算距离之前都要重置distance,很明显第一种简单一些,我这里用的是第二种方法。

参考百度地图的做法,当一条路线的距离小于1公里时,在详情页显示XX米;当一条路线的距离大于1公里时,则显示XX公里XX米,小数点后保留1位。格式转换写了一个小小的工具类,仅供参考:

[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1. // 距离转换  
  2. public static String distanceFormatter(int distance) {  
  3.     if (distance < 1000) {  
  4.         return distance + "米";  
  5.     } else if (distance % 1000 == 0) {  
  6.         return distance / 1000 + "公里";  
  7.     } else {  
  8.         DecimalFormat df = new DecimalFormat("0.0");  
  9.         int a1 = distance / 1000// 十位  
  10.   
  11.         double a2 = distance % 1000;  
  12.         double a3 = a2 / 1000// 得到个位  
  13.   
  14.         String result = df.format(a3);  
  15.         double total = Double.parseDouble(result) + a1;  
  16.         return total + "公里";  
  17.     }  
  18. }  


时间计算

说到时间计算,那么首先得到的应该是速度,通过距离/速度才能得到时间,关于速度这个东西我参考百度地图做了一组测试,在三环以内(因为高速的话速度肯定不一样,这里只计算市区)随机采集10公里左右的两个点,分别查询驾车、公交、步行所需要的时间,然后通过距离除速度的方式算出距离,依次类推,做了5组测试数据,最终取了平均值得到以下的结果:

步行1分钟 0.06公里
驾车1分钟 0.55公里
公交1分钟 0.15公里

当然这组数据不一定很精确,不过经过实际测试发现误差也在容忍范围之内。有了距离和速度就好办了,看50行,通过距离/速度最终得到了一条路线所需要耗费的时间。参考百度地图,时间小于60分钟时显示XX分钟,时间大于60分钟时显示XX小时XX分钟,所有这里自然也需要格式转换一下,下面是转换方法:

[java]   view plain copy 在CODE上查看代码片 派生到我的代码片
  1. // 时间转换  
  2. public static String timeFormatter(int minute) {  
  3.     if (minute < 60) {  
  4.         return minute + "分钟";  
  5.     } else if (minute % 60 == 0) {  
  6.         return minute / 60 + "小时";  
  7.     } else {  
  8.         int hour = minute / 60;  
  9.         int minute1 = minute % 60;  
  10.         return hour + "小时" + minute1 + "分钟";  
  11.     }  
  12.   
  13. }  

最后就是把路线详情(包括距离、时间和所有路段的ListView)展示在一个PopupWindow上就结束了,看一下模拟器的运行效果图(模拟器问题比较多,路线没画出来,不过整体流程没问题,后面会贴真机的运行截图):



下面就是真机操作截图,首先点击红色圆圈的Marker弹出下面的PopupWindow,再点击”到这去“,弹出上面的PopupWindow并自动填充起点和终点的位置信息。




加载完毕后生成路线图:



点击右侧的详情按钮可以查看详情,点击垃圾桶图标可以删除路线。

下面是同一条路线的三种不同规划的详情界面,分别是驾车、公交和步行:


源码下载 http://download.csdn.net/detail/leokelly001/8259097


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值