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

版权声明:本文为博主原创文章,转载请标明出处(http://blog.csdn.net/wlwlwlwl015)Thanks. https://blog.csdn.net/wlwlwlwl015/article/details/41091099



一、前言



转载请标明出处: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;
//初始化GeoCoder
mCoder = GeoCoder.newInstance();
mCoder.setOnGetGeoCodeResultListener(this);


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

	//Geo搜索
        @Override
	public void onGetGeoCodeResult(GeoCodeResult arg0) {
		// TODO Auto-generated method stub
	}

	// 反Geo搜索
	@Override
	public void onGetReverseGeoCodeResult(ReverseGeoCodeResult result) {
		// TODO Auto-generated method stub
	}
然后就可以在点击按钮的时候发起反地理编码请求(经纬度->地址信息)了:
	// 到这去
	class MyOnclickListenerTwo implements OnClickListener {
		@Override
		public void onClick(View v) {
			if (popupWindow != null && popupWindow.isShowing())
				popupWindow.dismiss();
			// 当前marker的坐标
			LatLng currentMarkerPosition = MyFragment.this.position;
			// 反Geo搜索
			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) {
		// TODO Auto-generated method stub
		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.DrivingStepTransitRouteLine.TransitStepWalkingRouteLine.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) {
		// TODO Auto-generated method stub
	}

	@Override
	public void onGetTransitRouteResult(TransitRouteResult arg0) {
		// TODO Auto-generated method stub	
	}

	@Override
	public void onGetWalkingRouteResult(WalkingRouteResult arg0) {
		// TODO Auto-generated method stub	
	}
然后就可以在点击按钮的时候发起路线查询的请求了:

// 发起路线规划搜索
	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();

			// 清空路线Result
			if (drResult != null)
				drResult = null;
			if (trResult != null)
				trResult = null;
			if (wrResult != null)
				wrResult = null;

			// 再标记一遍特色数据的marker
			addMarkers();

			// 设置起终点信息,对于tranist search 来说,城市名无意义
			PlanNode stNode = null;
			PlanNode enNode = null;
			String cityName = mLocation.getCity();
			if (position != null) {
				// 根据坐标查询 我的坐标 ---> 目标marker
				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) {
				// 显示Loading
				dialog.show();
				mSearch.drivingSearch((new DrivingRoutePlanOption()).from(
						stNode).to(enNode));
			} else if (v.getId() == R.id.transit) {
				// 显示Loading
				dialog.show();
				mSearch.transitSearch((new TransitRoutePlanOption())
						.from(stNode).city(cityName).to(enNode));
			} else if (v == btnWalk) {
				// 显示Loading
				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) {
		// 关闭Loading
		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 添加到地图上
			overlay.zoomToSpan(); // 缩放地图,使所有Overlay都在合适的视野内 注:
									// 该方法只对Marker类型的overlay有效
			// 显示清理marker的垃圾桶按钮
			imageButtonClear.setVisibility(View.VISIBLE);

			List<DrivingRouteLine> routeLines = result.getRouteLines();
			List<DrivingStep> steps = routeLines.get(0).getAllStep();
			// 分为N步
			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; // 叠加每一个step的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) {
					// TODO Auto-generated method stub
					// Show路线详情的PopupWindow
					showPopupWindowFour("驾车方案",
							MapUtil.distanceFormatter(distance),
							MapUtil.timeFormatter(needTime));

				}
			});
		}

	}

第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,很简单,只是修改了路线起点和终点的图标而已:

// 定制DrivingRouteOverly
	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并自动填充起点和终点的位置信息。




加载完毕后生成路线图:



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

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




三、总结



写到这里基本就已经完成了我们集成的LBS服务的所有功能,后面可能还需要加入离线地图,可以使用户在没有网络的情况下使用地图服务,这个我就不做记录了,鸿洋大神已经写过了离线地图的blog。我使用的版本是3.1.1,在上周五才更新了3.2.0,增加了热力图的相关功能,由于我们项目暂时用不到就不做过多的介绍了。百度地图SDK从接触到现在也有一周时间了,总的来说收获不小,官方API、示例Demo都很到位,在模仿百度地图的过程中我也感觉自己进步了许多,作为一个Android新手还需要更多的学习才行,我相信只要肯坚持、能吃苦,那么就没有实现不了的功能!继续努力,加油,Raito!

猜你喜欢

转载自blog.csdn.net/wlwlwlwl015/article/details/41091099