Skip to content

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 route
  • setAlternativeRouteActive(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 route
  • getTrafficDelayOnRoute(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 ViewPager
  • ViewPagerAdapter - 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.