一、前言
转载请标明出处: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是坐标转位置)。下面贴上代码,首先是初始化部分:
- GeoCoder mCoder = null;
-
- mCoder = GeoCoder.newInstance();
- mCoder.setOnGetGeoCodeResultListener(this);
我们的Activity或Fragment需要实现OnGetGeoCoderResultListener接口,并重写这两个回调方法,这里我们只需要用法反Geo搜索,所以重写onGetReverseGeoCodeResult方法即可。
-
- @Override
- public void onGetGeoCodeResult(GeoCodeResult arg0) {
-
- }
-
-
- @Override
- public void onGetReverseGeoCodeResult(ReverseGeoCodeResult result) {
-
- }
然后就可以在点击按钮的时候发起
反地理编码请求(经纬度->地址信息)了:
-
- class MyOnclickListenerTwo implements OnClickListener {
- @Override
- public void onClick(View v) {
- if (popupWindow != null && popupWindow.isShowing())
- popupWindow.dismiss();
-
- LatLng currentMarkerPosition = MyFragment.this.position;
-
- mCoder.reverseGeoCode(new ReverseGeoCodeOption()
- .location(currentMarkerPosition));
- dialog.show();
- }
- }
这里的MyFragment.this.position是我用来保存最后点击的特色数据坐标的全局变量,现在回头看一下OnMarkerClickListener中的代码(在上一篇blog)可以发现,每当触发Marker的点击事件时,都会将该Marker的position保存起来,而“到这去”的PopupWindow正是点击Marker之后才弹出的,所以通过这个变量自然可以得到当前Marker的坐标了。
最后通过reverseGeoCode方法,根据ReverseGeoCodeOption传入的坐标,即可完成地址查询请求的发起。
当查询完毕后,我们就可以在回调方法中获取到位置信息了,由于reverseGeoCode发起的是异步请求,所以我们应当在回调方法中去得到结果并展示:
- @Override
- public void onGetReverseGeoCodeResult(ReverseGeoCodeResult result) {
-
- if (result == null || result.error != SearchResult.ERRORNO.NO_ERROR) {
- Toast.makeText(getActivity(), "抱歉,未能找到结果", Toast.LENGTH_LONG)
- .show();
- return;
- }
- reverseGeoCodeResult = result.getAddress();
-
- String address = mLocation.getAddrStr();
- dialog.dismiss();
- showPopupWindowTwo(address, reverseGeoCodeResult);
- }
Part B.
有了起点和终点,接下来我们就可以通过点击三种不同的按钮分别去查询路线信息了。在百度的SDK中,路线是由RouteLine这个类表示的,它有三个直接子类,分别是DrivingRouteLine(驾车路线),TransitRouteLine(公交路线),WalkingRouteLine(步行路线),而每一条路线又是由多个路段组成的,在百度的SDK中,路段是由RouteStep这个类表示的,同理它也有几个直接子类,分别是DrivingRouteLine.DrivingStep,TransitRouteLine.TransitStep,WalkingRouteLine.WalkingStep。我们通常在路线检索的结果(RouteResult)中可以得到RouteLine对象,进而通过它的getAllStep()方法得到所有的Step并遍历依次获取路段的详细数据。
下面贴代码,同上首先是初始化部分:
-
- RouteLine route = null;
-
- RoutePlanSearch mSearch = null;
-
- mSearch = RoutePlanSearch.newInstance();
-
- mSearch.setOnGetRoutePlanResultListener(this);
我们的Activity或Fragment需要实现OnGetRoutePlanResultListener接口,并重写三种路线查询结果的回调方法:
- @Override
- public void onGetDrivingRouteResult(DrivingRouteResult arg0) {
-
- }
-
- @Override
- public void onGetTransitRouteResult(TransitRouteResult arg0) {
-
- }
-
- @Override
- public void onGetWalkingRouteResult(WalkingRouteResult arg0) {
-
- }
然后就可以在点击按钮的时候发起路线查询的请求了:
-
- class MySearchButtonProcessClickListener implements OnClickListener {
- @Override
- public void onClick(View v) {
- if (popupWindowTwo != null && popupWindowTwo.isShowing()) {
- popupWindowTwo.dismiss();
- }
-
-
- if (route != null)
- route = null;
- if (routeOverlay != null)
- routeOverlay.removeFromMap();
- mBaiduMap.clear();
-
-
- if (drResult != null)
- drResult = null;
- if (trResult != null)
- trResult = null;
- if (wrResult != null)
- wrResult = null;
-
-
- addMarkers();
-
-
- PlanNode stNode = null;
- PlanNode enNode = null;
- String cityName = mLocation.getCity();
- if (position != null) {
-
- stNode = PlanNode.withLocation(new LatLng(mLocation
- .getLatitude(), mLocation.getLongitude()));
- enNode = PlanNode.withLocation(position);
- } else {
-
- stNode = PlanNode.withCityNameAndPlaceName(cityName, eStart
- .getText().toString());
- enNode = PlanNode.withCityNameAndPlaceName(cityName, eEnd
- .getText().toString());
- }
- if (v.getId() == R.id.drive) {
-
- dialog.show();
- mSearch.drivingSearch((new DrivingRoutePlanOption()).from(
- stNode).to(enNode));
- } else if (v.getId() == R.id.transit) {
-
- dialog.show();
- mSearch.transitSearch((new TransitRoutePlanOption())
- .from(stNode).city(cityName).to(enNode));
- } else if (v == btnWalk) {
-
- dialog.show();
- mSearch.walkingSearch(new WalkingRoutePlanOption().from(stNode)
- .to(enNode));
- }
-
- }
-
- }
第10行到25行是一些 重置操作,毕竟每次绘制路线的时候都要清空关于上次绘制的信息,所以这里根据自己的情况去控制。可以看到33行和38行中我展示了两种设置起点终点的方法,分别是通过坐标查询和通过城市名查询,用任意一种都可以,官方提供了这两种选择。最后从43行开始,根据点击不同的按钮进入不同的分支,分别发起不同的路线查询。
Part C.
同Geo搜索类似,所有的搜索请求都是异步的,所以我们自然是在查询结果的回调方法中去获取结果数据了,由于这三种基本都是类似的所以我只写一下驾车的方案。驾车路线对应的回调方法是onGetDrivingRouteResult,在百度的SDK中查询出来的路线结果都是由SearchResult这个类表示的,那么自然的它也有以下的几个直接子类: DrivingRouteResult,TransitRouteResult,WalkingRouteResult,它们分别表示了这三种方案的查询结果。下面是c、d、e、f四个步骤的整体代码,先全部贴出来,再分段解释一下重点的部分:
-
- @Override
- public void onGetDrivingRouteResult(final DrivingRouteResult result) {
-
- dialog.dismiss();
- if (result == null || result.error != SearchResult.ERRORNO.NO_ERROR) {
- Toast.makeText(getActivity(), "抱歉,未找到结果", Toast.LENGTH_SHORT)
- .show();
- }
- if (result.error == SearchResult.ERRORNO.AMBIGUOUS_ROURE_ADDR) {
-
- return;
- }
- if (result.error == SearchResult.ERRORNO.NO_ERROR) {
-
- if (stepDetails.size() != 0)
- stepDetails.clear();
- if (distance != 0)
- distance = 0;
- if (needTime != 0)
- needTime = 0;
-
- route = result.getRouteLines().get(0);
- DrivingRouteOverlay overlay = new MyDrivingRouteOverlay(mBaiduMap);
- routeOverlay = overlay;
-
- mBaiduMap.setOnMarkerClickListener(new MyMarkerClickListener());
-
- overlay.setData(result.getRouteLines().get(0));
- overlay.addToMap();
- overlay.zoomToSpan();
-
-
- imageButtonClear.setVisibility(View.VISIBLE);
-
- List<DrivingRouteLine> routeLines = result.getRouteLines();
- List<DrivingStep> steps = routeLines.get(0).getAllStep();
-
- for (int i = 0; i < steps.size(); i++) {
- String instructions = steps.get(i).getInstructions();
- int direction = steps.get(i).getDirection();
- int distance = steps.get(i).getDistance();
- this.distance += distance;
- String entraceInstructions = steps.get(i)
- .getEntraceInstructions();
- String title = steps.get(i).getEntrace().getTitle();
- stepDetails.add((i + 1) + "." + instructions);
- }
-
- needTime = distance / 550;
-
-
- imageButtonShowDetail.setVisibility(View.VISIBLE);
- imageButtonShowDetail.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
-
-
- showPopupWindowFour("驾车方案",
- MapUtil.distanceFormatter(distance),
- MapUtil.timeFormatter(needTime));
-
- }
- });
- }
-
- }
第16行到第21行依旧是在绘制路线之前做的清理工作,它们分别是路线详情、路线的总距离和需要花费的总时间,自然应到在每次绘制新路线之前重置。第23行通过DrivingRouteResult的getRouteLines()得到所有的规法方案(方案往往不止一种,比如:最短时间的方案、最短距离的方案、少走高速的方案等等),由于我们集成的服务没必要细化到这种程度,所以通过get(0)获取到默认的方案即可。
第24行是一个关键点,体现了
Part D的内容。在百度地图SDK中,提供了一个用于显示和管理多个覆盖物的类——OverlayManager,同样的它的直接子类有DrivingRouteOverlay, TransitRouteOverlay,WalkingRouteOverlay。所以我们如果要在地图是绘制出路线的Overlay,那么无疑是要通过OverlayManager的这些子类去实现的。可是我们在24行看到的是newMyDrivingRouteOverlay(mBaiduMap)而不是DrivingRouteOverlay,那么没错,这个类也是我们自己定制的。那么自定义的DrivingRouteOverlay究竟能做些什么呢,我们打开官方给出的API可以发现:
文档中写的很清楚重写这些方法分别能干什么了,那么我们根据需求去重写它们即可:
如果不需要上面的这些功能就直接去new一个DrivingRouteOverlay即可,下面贴出我的自定义类——MyDrivingRouteOverlay,很简单,只是修改了路线起点和终点的图标而已:
-
- private class MyDrivingRouteOverlay extends DrivingRouteOverlay {
-
- public MyDrivingRouteOverlay(BaiduMap baiduMap) {
- super(baiduMap);
- }
-
- @Override
- public BitmapDescriptor getStartMarker() {
- if (useDefaultIcon) {
- return BitmapDescriptorFactory.fromResource(R.drawable.icon_st);
- }
- return null;
- }
-
- @Override
- public BitmapDescriptor getTerminalMarker() {
- if (useDefaultIcon) {
- return BitmapDescriptorFactory.fromResource(R.drawable.icon_en);
- }
- return null;
- }
- }
第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位。格式转换写了一个小小的工具类,仅供参考:
-
- public static String distanceFormatter(int distance) {
- if (distance < 1000) {
- return distance + "米";
- } else if (distance % 1000 == 0) {
- return distance / 1000 + "公里";
- } else {
- DecimalFormat df = new DecimalFormat("0.0");
- int a1 = distance / 1000;
-
- double a2 = distance % 1000;
- double a3 = a2 / 1000;
-
- String result = df.format(a3);
- double total = Double.parseDouble(result) + a1;
- return total + "公里";
- }
- }
时间计算
说到时间计算,那么首先得到的应该是速度,通过距离/速度才能得到时间,关于速度这个东西我参考百度地图做了一组测试,在三环以内(因为高速的话速度肯定不一样,这里只计算市区)随机采集10公里左右的两个点,分别查询驾车、公交、步行所需要的时间,然后通过距离除速度的方式算出距离,依次类推,做了5组测试数据,最终取了平均值得到以下的结果:
步行1分钟 0.06公里
驾车1分钟 0.55公里
公交1分钟 0.15公里
当然这组数据不一定很精确,不过经过实际测试发现误差也在容忍范围之内。有了距离和速度就好办了,看50行,通过距离/速度最终得到了一条路线所需要耗费的时间。参考百度地图,时间小于60分钟时显示XX分钟,时间大于60分钟时显示XX小时XX分钟,所有这里自然也需要格式转换一下,下面是转换方法:
-
- public static String timeFormatter(int minute) {
- if (minute < 60) {
- return minute + "分钟";
- } else if (minute % 60 == 0) {
- return minute / 60 + "小时";
- } else {
- int hour = minute / 60;
- int minute1 = minute % 60;
- return hour + "小时" + minute1 + "分钟";
- }
-
- }
最后就是把路线详情(包括距离、时间和所有路段的ListView)展示在一个PopupWindow上就结束了,看一下模拟器的运行效果图(模拟器问题比较多,路线没画出来,不过整体流程没问题,后面会贴真机的运行截图):
下面就是真机操作截图,首先点击红色圆圈的Marker弹出下面的PopupWindow,再点击”到这去“,弹出上面的PopupWindow并自动填充起点和终点的位置信息。
加载完毕后生成路线图:
点击右侧的详情按钮可以查看详情,点击垃圾桶图标可以删除路线。
下面是同一条路线的三种不同规划的详情界面,分别是驾车、公交和步行:
源码下载 http://download.csdn.net/detail/leokelly001/8259097