09 Alternatives
Overview
This tutorial will show you how to calculate a route with two alternative route proposals.
Base
You can use your results from the previous Tutorial08
as a base for our new project.
Route overview
First we will implement a new Activity showing the complete route with some metrics.
We call the activity RouteInfoActivity
and the xml activity_route_info
. The layout basically consists of a RelativeLayout with four buttons for the metrics and a MapView showing the current route trace. You will find all needed graphics for the metric buttons in the res
folder of Tutorial09
.
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.ptvag.navigation.tutorial.RouteInfoActivity"> <RelativeLayout android:id="@+id/infoMain" android:layout_width="match_parent" android:layout_height="100dp" android:background="@color/colorPrimaryDark" android:gravity="top"> <Button android:id="@+id/distanceInfo" style="@style/Widget.AppCompat.Button.Borderless" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableTop="@drawable/arw_destination_dist" android:gravity="center_horizontal|bottom" android:textAllCaps="false" android:text="2.3 km" /> <Button android:id="@+id/timeInfo" style="@style/Widget.AppCompat.Button.Borderless" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/distanceInfo" android:drawableTop="@drawable/arw_destination" android:gravity="center_horizontal|bottom" android:textAllCaps="false" android:text="25 min" /> <Button android:id="@+id/trafficInfo" style="@style/Widget.AppCompat.Button.Borderless" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/timeInfo" android:drawableTop="@drawable/icon_traffic_blocked" android:gravity="center_horizontal|bottom" android:textAllCaps="false" android:text="10 min" /> <Button android:id="@+id/tollInfo" style="@style/Widget.AppCompat.Button.Borderless" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/trafficInfo" android:drawableTop="@drawable/icon_toll" android:gravity="center_horizontal|bottom" android:textAllCaps="false" android:text="500 m" /> </RelativeLayout> <com.ptvag.navigation.tutorial.MapView android:id="@+id/map" android:layout_below="@id/infoMain" android:layout_width="match_parent" android:layout_height="match_parent"/> <android.support.design.widget.FloatingActionButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:src="@android:drawable/ic_dialog_map" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:onClick="onFloatingButtonClicked"/> </RelativeLayout>
This code should look familiar to you. We just implement an Activity with a MapView inside. The new thing is that we call MapView.zoom2Route()
in the onInitialize
listener. Don't forget to register the RouteInfoActivity
in the AndroidManifest.xml
. We start the Activity in the MainActivity when the route calculation was successful (instead of calling navigationLoop.startNavigation()
). Now we should see our RouteInfoActivity with a centered route trace.
public class RouteInfoActivity extends DrawerActivity implements OnInitializeListener { private MapView mapView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_route_info); mapView = (MapView)findViewById(R.id.map); mapView.setOnInitializeListener(this); Button distanceInfo = (Button)findViewById(R.id.distanceInfo); Button timeInfo = (Button)findViewById(R.id.timeInfo); Button trafficInfo = (Button)findViewById(R.id.trafficInfo); Button tollInfo = (Button)findViewById(R.id.tollInfo); } @Override public void onResume() { super.onResume(); mapView.resume(); } @Override public void onPause() { super.onPause(); mapView.finish(); } @Override public void onInitialize() { mapView.zoom2Route(); mapView.update(); } @Override public void onFirstTimeDrawn() { } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.ptvag.navigation.tutorial"> ... <application ... <activity android:name=".SearchActivity"></activity> <activity android:name=".ResultListActivity"></activity> <activity android:name=".RouteInfoActivity"></activity> <activity android:name=".TrafficActivity"></activity> </application> </manifest>
public class MainActivity extends DrawerActivity implements OnInitializeListener { ... void handleIntent(Intent intent) { ... Runnable calcRoute = new Runnable() { @Override public void run() { ... try { Tour tour = new Tour(start); tour.addStation(new WayPoint(destination)); CalculateTourResult result = NavigationSDK.calculateTour(tour, calcRouteObserver); if (result.getError() != SDKErrorCode.Ok) { Log.v("Error", result.getError().toString()); } else { Intent intent = new Intent(MainActivity.this, RouteInfoActivity.class); startActivity(intent); } } catch (CalculateTourException | NavigationException e) { e.printStackTrace(); } } }; ... } ... }
Now let us fill the metric buttons with some values. For this purpose we show from left to right the distance to destination, the estimated time of arrival, the delay caused by traffic and the covered distance on toll roads. All these values can be obtained by calling NavigationSDK.getRouteInformation()
. The RouteInformation class contains information like distance, duration, toll and restrictions on your current route. To access the traffic delay, we simply call NavigationSDK.getTrafficDelayOnRoute()
. Like in Tutorial06
we format the distances with NavigationSDK.getLocalizedDistanceString
.
public class RouteInfoActivity extends DrawerActivity implements OnInitializeListener { ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_route_info); mapView = (MapView)findViewById(R.id.map); mapView.setOnInitializeListener(this); Button distanceInfo = (Button)findViewById(R.id.distanceInfo); Button timeInfo = (Button)findViewById(R.id.timeInfo); Button trafficInfo = (Button)findViewById(R.id.trafficInfo); Button tollInfo = (Button)findViewById(R.id.tollInfo); RouteInformation info = NavigationSDK.getRouteInformation(0, 0); distanceInfo.setText(NavigationSDK.getLocalizedDistanceString(info.getLength(), false)); tollInfo.setText(NavigationSDK.getLocalizedDistanceString(info.getTollInformation().getTollLength(), false)); Calendar destCalendar = Calendar.getInstance(); destCalendar.add(Calendar.SECOND, (int)info.getDuration()); SimpleDateFormat formatter = new SimpleDateFormat("hh:mm"); timeInfo.setText(formatter.format(destCalendar.getTime())); long trafficTime = NavigationSDK.getTrafficDelayOnRoute(true, 0); long hours = trafficTime / 3600; long minutes = (trafficTime % 3600) / 60; trafficInfo.setText("+ " + hours + " h " + minutes + " min"); } ... }
Finally we need a way to switch back to the MainActivity and start the navigation. For this we just send a startDestination intent to the MainActivity when the floating action button is clicked.
Attention
We start the activity with the flags Intent.FLAG_ACTIVITY_CLEAR_TOP and Intent.FLAG_ACTIVITY_SINGLE_TOP. This will reuse the current MainActivity instead of instantiating a new instance.
public class RouteInfoActivity extends DrawerActivity implements OnInitializeListener { ... public void onFloatingButtonClicked(View view) { Intent intent = new Intent(this, MainActivity.class); intent.putExtra("startNavigation", "foo"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } }
Now we add code for handling the startNavigation
intent in our MainActivity and call navigationLoop.startNavigation()
to have a fully working navigation again.
public class MainActivity extends DrawerActivity implements OnInitializeListener { ... void handleIntent(Intent intent) { if (intent.hasExtra("startNavigation")) { navigationLoop.startNavigation(); intent.removeExtra("startNavigation"); return; } ... } ... }
Enabling alternative routes
Enabling alternatives for the route calculation is really easy: Just set the RouteCalculationType to RouteAlternative in the setDefaultRoutingOptions
of the Application class.
public class Application extends android.app.Application { @Override public void onCreate() { super.onCreate(); ... setDefaultRoutingOptions(); } ... private void setDefaultRoutingOptions() { try { Result<RouteOptions> result = NavigationSDK.getRoutingOptions(); RouteOptions options = result.getResult(); options.setRouteCalculationType(RouteCalculationType.RouteAlternative); ... } ... } ... }
Switching alternative routes
Finally we want to have a mechanism to switch between all given alternatives. The following NavgationSDK methods are helpful for us:
getAlternativeRouteCount()
- receive the amount of alternative routes (including the default, currently <= 3)getAlternativeRouteActive()
- receive the currently active alternative routesetAlternativeRouteActive(alt)
- set the active alternative route (also highlights the active route trace on the map)getRouteInformation(0, alt)
- obtain the RouteInformation for the specified alternative routegetTrafficDelayOnRoute(true, alt)
- obtain the delay caused by traffic on the selected alternative route
To show all calculated alternative routes, we will modify our RouteInfoActivity. It will contain up to three tabs, one for each alternative route. The description of the UI needed for the tabbed display is beyond the scope of this tutorial. The helper module (introduced in an earlier tutorial) provides two classes which assist us with adding the tabs.
TabsActivity
- an activity combining the drawer functionality with tabs and a ViewPagerViewPagerAdapter
- an adapter we can fill with fragments the TabsActivity will load in their tabs
First we have to convert our RouteInfoActivity into a Fragment. We rename it to RouteInfoFragment and also rename the layout to fragment_route_info
. After that we have to move our code from onCreate to onCreateView
, inflate our layout by ourself and operate on the resulting view. Thats all to get a working Fragment.
... import android.support.v4.app.Fragment; ... public class RouteInfoFragment extends Fragment implements OnInitializeListener { private MapView mapView; public RouteInfoFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_route_info, container, false); mapView = (MapView)view.findViewById(R.id.map); mapView.setOnInitializeListener(this); Button distanceInfo = (Button)view.findViewById(R.id.distanceInfo); Button timeInfo = (Button)view.findViewById(R.id.timeInfo); Button trafficInfo = (Button)view.findViewById(R.id.trafficInfo); Button tollInfo = (Button)view.findViewById(R.id.tollInfo); RouteInformation info = NavigationSDK.getRouteInformation(0, 0); distanceInfo.setText( NavigationSDK.getLocalizedDistanceString(info.getLength(), false) ); tollInfo.setText( NavigationSDK.getLocalizedDistanceString( info.getTollInformation().getTollLength(), false ) ); Calendar destCalendar = Calendar.getInstance(); destCalendar.add(Calendar.SECOND, (int)info.getDuration()); SimpleDateFormat formatter = new SimpleDateFormat("hh:mm"); timeInfo.setText(formatter.format(destCalendar.getTime())); long trafficTime = NavigationSDK.getTrafficDelayOnRoute(true, 0); long hours = trafficTime / 3600; long minutes = (trafficTime % 3600) / 60; trafficInfo.setText("+ " + hours + " h " + minutes + " min"); return view; } @Override public void onResume() { super.onResume(); mapView.resume(); } @Override public void onPause() { super.onPause(); mapView.finish(); } @Override public void onInitialize() { updateRouteView(); } @Override public void onFirstTimeDrawn() { } public void updateRouteView() { if (mapView != null) { mapView.zoom2Route(); mapView.update(); } } }
Now we need a new RouteInfoActivity which holds our newly created fragment. To add a fragment view, simply override setupViewPager
and pass a ViewPageAdapter
with a new RouteInfoFragment. Additionally we move the onFloatingButtonClicked
from our Fragment to this Activity.
public class RouteInfoActivity extends TabsActivity { @Override protected void setupViewPager(ViewPager viewPager) { ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager()); adapter.addFragment(new RouteInfoFragment(), "title"); viewPager.setAdapter(adapter); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); inflateNavigationMenu(R.menu.drawer_menu); } public void onFloatingButtonClicked(View view) { Intent intent = new Intent(this, MainActivity.class); intent.putExtra("startNavigation", "foo"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } }
After this we should have a RouteInfoActivity containing one tab with the same behaviour as before.
We increase the amount of Fragments in RouteInfoActivity to the amount given by NavigationSDK.getAlternativeRouteCount()
, give the fragments the alternative index as an argument and replace the title of the tabs with the formatted total duration of the alternative by calling NavigationSDK.getRouteInformation(0, alternative).getDuration()
Now we can alter our RouteInfoFragment to represent the alternatives. We introduce a setter for the index of the alternative in RouteInfoFragment and increase the amount of Fragments in RouteInfoActivity to the amount given by NavigationSDK.getAlternativeRouteCount()
. Also we replace the title of the tabs with the formatted total duration of the alternative by calling NavigationSDK.getRouteInformation(0, alternative).getDuration()
.
public class RouteInfoActivity extends TabsActivity { ViewPagerAdapter adapter; @Override protected void setupViewPager(ViewPager viewPager) { adapter = new ViewPagerAdapter(getSupportFragmentManager()); int count = NavigationSDK.getAlternativeRouteCount(); for (int i = 0; i < count; ++i) { long duration = NavigationSDK.getRouteInformation(0, i).getDuration(); Bundle args = new Bundle(); args.putInt("alternative", i); Fragment fragment = new RouteInfoFragment(); fragment.setArguments(args); adapter.addFragment(fragment, format(duration)); } viewPager.setAdapter(adapter); } String format(long duration){ long hours = duration / 3600; long minutes = (duration % 3600) / 60; return "" + hours + " h " + minutes + " min"; } ... }
The next thing to do is to read the alernative index form the fragments arguments and replace the routeInformation calls in RouteInfoFragment with their alternative route counterparts.
public class RouteInfoFragment extends Fragment implements OnInitializeListener { ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_route_info, container, false); int alternative = getArguments().getInt("alternative", 0); mapView = (MapView)view.findViewById(R.id.map); mapView.setOnInitializeListener(this); Button distanceInfo = (Button)view.findViewById(R.id.distanceInfo); Button timeInfo = (Button)view.findViewById(R.id.timeInfo); Button trafficInfo = (Button)view.findViewById(R.id.trafficInfo); Button tollInfo = (Button)view.findViewById(R.id.tollInfo); RouteInformation info = NavigationSDK.getRouteInformation(0, alternative); distanceInfo.setText(NavigationSDK.getLocalizedDistanceString(info.getLength(), false)); tollInfo.setText(NavigationSDK.getLocalizedDistanceString(info.getTollInformation().getTollLength(), false)); Calendar destCalendar = Calendar.getInstance(); destCalendar.add(Calendar.SECOND, (int)info.getDuration()); SimpleDateFormat formatter = new SimpleDateFormat("hh:mm"); timeInfo.setText(formatter.format(destCalendar.getTime())); long trafficTime = NavigationSDK.getTrafficDelayOnRoute(true, alternative); long hours = trafficTime / 3600; long minutes = (trafficTime % 3600) / 60; trafficInfo.setText("+ " + hours + " h " + minutes + " min"); return view; } ... }
The last part missing is that the current chosen alternative is highlighted in each tab. So we have to call NavigationSDK.setAlternativeRouteActive(alternative);
on each page change and refresh the MapView in the current page.
public class RouteInfoActivity extends TabsActivity { ... @Override protected void setupViewPager(ViewPager viewPager) { ... viewPager.setAdapter(adapter); viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int i, float v, int i1) {} @Override public void onPageSelected(int alternative) { Fragment fragment = adapter.getItem(alternative); NavigationSDK.setAlternativeRouteActive(alternative); if (fragment instanceof RouteInfoFragment) { ((RouteInfoFragment)fragment).updateRouteView(); } } @Override public void onPageScrollStateChanged(int i) {} }); }
This is our final result:
Attention
As soon as getGuidanceInformation
is called for the first time, the currently selected alternative will be the only selectable alternative. The SDK will remove the unwanted alternatives automatically.
Implications on navigation
Starting a navigation with an alternative route (i.e. bigger than 0) has implications to navigation. Because each of the alternative routes is not the arithmetical optimal route, there is a risk that rerouting will switch back to the optimal route. Therefore a special mechanism is implemented: The chosen route will be stored during the whole navigation. Its underlying segments get a small preference while rerouting. This should lead to a good trade-off finding reasonable reroutes, but also most likely keeps the chosen alternative route.