Skip to content

06 Navigating a route

Overview

In this tutorial you will learn how to implement route navigation and how to interpret the maneuvers given by the NavigationSDK. We also show you an easy way to simulate a navigation along the current route for testing.

Base

Use Tutorial05 as a starting point for this project.

getGuidanceInformation

To get maneuvers for our current guidance, we have to call getGuidanceInformation() in our NavigationLoop. But we only have to call getGuidanceInformation() when we are navigating a route. So we add an isNavigation flag to our NavigationLoop to determine the status of our app. We use an AtomicBoolean to guarantee thread save access to it.

public class NavigationLoop extends Runnable {

    ...

    AtomicBoolean isNavigation = new AtomicBoolean(false);

    public void startNavigation() {
        isNavigation.set(true);
    }

    public void stopNavigation() {
        isNavigation.set(false);
    }

    public boolean isNavigation() {
        return isNavigation.get();
    }
}

We can call NavigationLoop.startNavigation() after calculateTour() returns no errors in the java.lang.Runnable in our MainActivy.

try {
    CalculateTourResult result =
            NavigationSDK.calculateTour(tour, calcRouteObserver);

    if (result.getError() != SDKErrorCode.Ok) {
        Toast.makeText(MainActivity.this, "route calculation failed", Toast.LENGTH_LONG).show();
    } else {
        navigationLoop.startNavigation();
    }
} catch (CalculateTourException | NavigationException e) {
    e.printStackTrace();
}

Now we can get the guidance information in the navigation loop. Because getGuidanceInformation() is a potentially long running operation, we usually have to call it in a java.lang.Runnable and push it to our SDKJobQueue. But since we are already in a java.lang.Runnable - which is called in the SDKJobQueue - there is no need for this. It wouldn't make sense to get guidance information if we have no route. So we check this and return early from the loop. Finally we also have a look at the getDestinationReached() flag of the returned NavigationInformation result to check if we have to stop the navigation.

public class NavigationLoop extends Runnable {
    ...

    @Override
    public void run() {
        try
        {
            ...
            if (isNavigation()) {
                final Result<NavigationInformation> result
                        = NavigationSDK.getGuidanceInformation(gps);

                if (result.getResult().getDestinationReached()) {
                    stopNavigation();
                }
            }
        }
        catch (NavigationException e)
        {
        }
    }

    ...
}

Rerouting

So what happens if we deviate from the given route? Do we have to check this and calculate a new route? No we don't, getGuidanceInformation() will take care of this. If we deviate only a few meters from our current route, the call to getGuidanceInformation() will pull us back to the route. If we deviate further, getGuidanceInformation() will calculate a complete new route to our destination.

Notifying all Controls in the NavigationLoop

We update our NavigationControl interface with three new methods.

interface NavigationControl {
    ...

    /**
     * Callback to notify controls of the current state of the guidance
     */ 
    void onNavigation(final NavigationInformation info, final GPSData gps); 

    /**
     * Callback to notify controls the navigation has started
     */
    void onNavigationStarted(final RouteInformation info);

    /**
     * Callback to notify controls the navigation has stopped
     */
    void onNavigationStopped();
}

We use these callbacks to notify all our controls with the current guidance information and when navigation started or stopped. For the start callback we also report the current RouteInformation of the current navigation.

class NavigationLoop implements Runnable {
    private final Handler handler;

    @Override
    public void run() {
        ...
            if (isNavigation()) {
                final Result<NavigationInformation> result = NavigationSDK.getGuidanceInformation(gps);

                if (result.getError() == SDKErrorCode.Ok) {
                    for (NavigationControl control : controls) {
                        control.onNavigation(result.getResult(), gps);
                    }
                    ...
                }
            }
        ...
    }

    ...

    public void startNavigation() {
        isNavigation.set(true);

        for (NavigationControl control : controls) {
            int activeTrace = NavigationSDK.getAlternativeRouteActive();
            control.onNavigationStarted(NavigationSDK.getRouteInformation(0, activeTrace));
        }
    }

    public void stopNavigation() {
        isNavigation.set(false);

        try {
            NavigationSDK.trackingMode(true, 0, 0);
        } catch (NavigationException e) {
            e.printStackTrace();
        }

        for (NavigationControl control : controls) {
            control.onNavigationStopped();
        }
    }

Attention

Don't update the GUI from the NavigationLoop. Pleas update the user interface only from the GUI thread (aka main thread).

The application would crash if we update our controls from the GUI thread instead of the SDKJobQueue thread. The easiest way to avoid this is to create a Handler in our MainActivity and pass it to our NavigationLoop and then call the callbacks in three Runnable which are posted to this Handler.

class NavigationLoop implements Runnable {
    private final Handler handler;
    private final List<NavigationControl> controls = new ArrayList<>();

    public NavigationLoop(Handler handler) {
        this.handler = handler;
    }

    @Override
    public void run() {
        ...
        if (isNavigation()) {
            final Result<NavigationInformation> result = NavigationSDK.getGuidanceInformation(gps);
            if (result.getError() == SDKErrorCode.Ok) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        for (NavigationControl control : controls) {
                            control.onNavigation(result.getResult(), gps);
                        }
                    }
                });
            ...
        }
        ...
    }

    ...

    public void startNavigation() {
        isNavigation.set(true);

        handler.post(new Runnable() {
            @Override
            public void run() {
                for (NavigationControl control : controls) {
                    int activeTrace = NavigationSDK.getAlternativeRouteActive();
                    control.onNavigationStarted(NavigationSDK.getRouteInformation(0, activeTrace));
                }
            }
        });
    }

    public void stopNavigation() {
        isNavigation.set(false);

        try {
            NavigationSDK.trackingMode(true, 0, 0);
        } catch (NavigationException e) {
            e.printStackTrace();
        }

        handler.post(new Runnable() {
            @Override
            public void run() {
                for (NavigationControl control : controls) {
                    control.onNavigationStopped();
                }
            }
        });
    }
    ...

}
public class MainActivity extends DrawerActivity implements OnInitializeListener, NavigationControl {
    ...
    private final Handler handler = new Handler();
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        navigationLoop = new NavigationLoop(handler);
        ...
    }

Building maneuvers

Here we will implement a small control wich shows the distance, the street name and an icon representation of the next maneuver.

The control consists of a RelativeLayout with a Button and a TextView inside, which we include in our activity_main.xml. To avoid the default Button border of the Material Design we set the style of the Button to Widget.AppCompat.Button.Borderless.

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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.MainActivity">

    <com.ptvag.navigation.tutorial.MapView
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="@+id/activity_main"
        app:layout_constraintLeft_toLeftOf="@+id/activity_main"
        app:layout_constraintRight_toRightOf="@+id/activity_main"
        app:layout_constraintTop_toTopOf="@+id/activity_main" />

    <RelativeLayout
        android:id="@+id/maneuverMain"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_alignParentTop="true"
        android:background="@color/colorPrimaryDark"
        android:gravity="top"
        android:visibility="visible">

        <Button
            android:id="@+id/maneuverBtn"
            style="@style/Widget.AppCompat.Button.Borderless"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:drawableTop="@drawable/arw_fwd_left"
            android:gravity="center_horizontal|bottom"
            android:text="500m" />

        <TextView
            android:id="@+id/maneuverText"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_toRightOf="@id/maneuverBtn"
            android:gravity="center_vertical|center_horizontal"
            android:padding="10dp"
            android:text="Haid-und-Neu-Straße 15"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Large" />
    </RelativeLayout>

    <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:layout_margin="16dp"
        android:onClick="onFloatingButtonClicked"
        android:src="@android:drawable/ic_dialog_map"
        app:layout_anchor="@id/map"
        app:layout_anchorGravity="bottom|right|end" />

</android.support.design.widget.CoordinatorLayout>

For managing the content and visibility of this Control we implement a class called NextManeuverControl which implements the NavigationControl interface and is based on a RelativeLayout, the Button and a TextView as members. We give it a method bindView() to refresh these members from a given View. We update the TextView with the street name of the next maneuver in our implementation of void onNavigation(final NavigationInformation info, final GPSData gps). We initialize the control as View.GONE in the bindView() method and show it in the public void onNavigationStarted(RouteInformation info) callback. Similar we will hide it again in public void onNavigationStopped().

public class NextManeuverControl {
    private ViewGroup main;
    private Button arrow;
    private TextView streetName;

    public NextManeuverControl() {
    }

    public void bindView(ViewGroup view) {
        main = view;
        arrow = (Button) main.findViewById(R.id.maneuverBtn);
        streetName = (TextView) main.findViewById(R.id.maneuverText);
        main.setVisibility(View.GONE);
    }

    @Override
    public void onGPS(GPSData gps) {
    }

    public void onNavigation(final NavigationInformation info, final GPSData gps) {
        streetName.setText(info.getNextManeuver().getStreetName());
    }

    @Override
    public void onNavigationStarted(RouteInformation info) {
        main.setVisibility(View.VISIBLE);
    }

    @Override
    public void onNavigationStopped() {
        main.setVisibility(View.GONE);
    }
}

To use the NextManeuverControl we create a member in the MainActivity and call bindView with the base RelativeLayout in onCreate of the MainActivity.

public class MainActivity extends AppCompatActivity  implements OnInitializeListener {
    NavigationLoop navigationLoop;
    NextManeuverControl nextManeuverControl = new NextManeuverControl();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        nextManeuverControl.bindView((ViewGroup)findViewById(R.id.maneuverMain));
        ...
        navigationLoop = new NavigationLoop(handler);

        ...
    }
}

To update the button of the control with a maneuver arrow, we have to interpret the ManeuverDescription given by getNextManeuver() and the CrossingView given by getNextManeuverCrossingView().

We implement an example how to map these informations into a maneuver arrow in a new method getDrawable in the NextManeuverControl using NavigationSDK.getManeuverArrow. The returned Drawable is set to the button with setCompoundDrawablesWithIntrinsicBounds. You will find all example arrows in the res folder of Tutorial06. In addition we set the distance to the maneuver as the text of the Button. To get a human readable string of NavigationInformation.getDistToNextManeuver(), we pass it to NavigationSDK.getLocalizedDistanceString().

    @Override
   public void onNavigation(final NavigationInformation info, final GPSData gps){
       if (!info.getDestinationReached()) {
           main.setVisibility(View.VISIBLE);
       }

       arrow.setCompoundDrawablesWithIntrinsicBounds(0, getDrawable(info.getNextManeuver(), info.getNextManeuverCrossingView()), 0, 0);

       arrow.setText(NavigationSDK.getLocalizedDistanceString(info.getDistToNextManeuver(), true, true, "now"));
       streetName.setText(info.getNextManeuver().getStreetName());
   }

   private int getDrawable(ManeuverDescription description, CrossingView crossingView) {
       ManeuverArrow arrow = NavigationSDK.getManeuverArrow(description, crossingView);

       switch (arrow) {
           case None:
               return R.drawable.arw_fwd;
           case Left:
               return R.drawable.arw_left;
           case Right:
               return R.drawable.arw_right;
           case FowardLeft:
               return R.drawable.arw_fwd_left;
           case FowardRight:
               return R.drawable.arw_fwd_right;
           case UTurnLeft:
               return R.drawable.arw_uturn_left;
           case UTurnRight:
               return R.drawable.arw_uturn_right;
           case WeakLeft:
               return R.drawable.arw_fwd_left_weak;
           case WeakRight:
               return R.drawable.arw_fwd_right_weak;
           case RoundaboutRight045:
               return R.drawable.arw_rb_right1;
           case RoundaboutRight090:
               return R.drawable.arw_rb_right2;
           case RoundaboutRight135:
               return R.drawable.arw_rb_right3;
           case RoundaboutRight180:
               return R.drawable.arw_rb_right4;
           case RoundaboutRight225:
               return R.drawable.arw_rb_right5;
           case RoundaboutRight270:
               return R.drawable.arw_rb_right6;
           case RoundaboutRight315:
               return R.drawable.arw_rb_right7;
           case RoundaboutLeft045:
               return R.drawable.arw_rb_left7;
           case RoundaboutLeft090:
               return R.drawable.arw_rb_left6;
           case RoundaboutLeft135:
               return R.drawable.arw_rb_left5;
           case RoundaboutLeft180:
               return R.drawable.arw_rb_left4;
           case RoundaboutLeft225:
               return R.drawable.arw_rb_left3;
           case RoundaboutLeft270:
               return R.drawable.arw_rb_left2;
           case RoundaboutLeft315:
               return R.drawable.arw_rb_left1;
           case Foward:
               return R.drawable.arw_fwd;
           case LeaveMotorWayLeft:
               return R.drawable.arw_mw_left;
           case LeaveMotorWayRight:
               return R.drawable.arw_mw_right;
           case Destination:
               return R.drawable.arw_destination;
           default:
               return R.drawable.arw_fwd;
       }
   }

Simulate driving

For easy testing, the NavigationSDK brings its own simulation mechanism. To use it we simply generate a simulation file by calling NavigationSDK.createGPSSimulation() after calculating a route. Afterwards we close our current GPSDevice by calling GPSManager.getInstance().closeGPSDevice() and reopen a simulated GPSDevice with GPSManager.getInstance().openSimulatedGPSDevice().

Attention

Don't push locations to the SDK by calling GPSManager.getInstance().pushLocation() while simulating! Please only retrieve the current position with GPSManager.getInstance().getCurrentPosition(). The simulation will step to the next position with every call to getCurrentPosition().

To start a simulation, we will introduce a drawer menu to our tutorial with an entry to start the simulation.

The implementation of an android drawer is out of the scope of this tutorial. So we deliver a helper module for this. For integration, simply copy the helper folder from our tutorial sources to your base project and add the helper module to the settings.gradle and as a dependency to your tutorial module gradle.build file.

settings.gradle

include ':helper'
build.gradle
...
dependencies {
    ...
    implementation project(':helper')
}

Attention

For the helper module we need a base style without an ActionBar. Therefore please set the parent of your main style in the styles.xml to Theme.AppCompat.NoActionBar.

With our helper module we just have to change the base class of the MainActivity to DrawerActivity. Since the helper now declares the Actionbar, we need to change the base theme to Theme.AppCompat.NoActionBar. Also we need a drawer_menu.xml in res/menu with the simulation entry. We inflate it by calling inflateNavigationMenu(R.menu.drawer_menu) in the MainActivities onCreate method. By overriding onNavigationItemSelected we can respond to a click in the drawer menu. To avoid calling pushLocation, we also introduce an AtomicBoolean called simulation which is toggled by clicking on simulation.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@+id/menu_simulation" android:title="toggle simulation"></item>
</menu>
public class MainActivity extends DrawerActivity implements OnInitializeListener {
    ...
    private AtomicBoolean simulation = new AtomicBoolean(false);

    private LocationListener locationListener = new LocationListener() {
        @Override
        public void onLocationChanged(final Location location) {
            SDKJobQueue.getInstance().push(new Runnable() {
                @Override
                public void run() {
                    if (!simulation.get()) {
                        GPSManager.getInstance().pushLocation(location);
                    }
                }
            });
        }
        ...
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        inflateNavigationMenu(R.menu.drawer_menu);
        ...
    }

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {

        switch (item.getItemId()){
            case R.id.menu_simulation:
                if (navigationLoop.isNavigation()) {
                    try {
                        if (simulation.get()) {
                            simulation.set(false);
                            GPSManager.getInstance().closeGPSDevice();
                            GPSManager.getInstance().openGPSDevice(true);
                        } else {
                            File simFilePath = new File(Application.getDataPath(), "simulation.dat");
                            NavigationSDK.createGPSSimulation(simFilePath);
                            GPSManager.getInstance().closeGPSDevice();
                            GPSManager.getInstance().openSimulatedGPSDevice(true, simFilePath);
                            simulation.set(true);
                        }
                    } catch (NavigationException e) {
                        e.printStackTrace();
                    }
                }
                break;
        }

        return super.onNavigationItemSelected(item);
    }

    ...
}

Adding Information Controls

What about adding some more information about our current Navigation? Lets add a View showing the current ETA (estimated time of arrival), the current ETE (estimated time enroute) or the current distance to the current target.

To show ETA, ETE and distance we design a Layout control_info.xml which holds a value, the unit and the description of the control.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigationInfo1"
    android:layout_width="0pt"
    android:layout_height="50dp"
    android:layout_weight="1"
    tools:showIn="@layout/activity_main">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true">

        <TextView
            android:id="@+id/value"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:paddingBottom="5dp"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Large"
            android:textColor="#ffffffff"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/unit"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:paddingBottom="5dp"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
            android:textColor="#ffffffff"
            android:textStyle="bold" />
    </LinearLayout>

    <TextView
        android:id="@+id/description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:gravity="center"
        android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
        android:textColor="#ffffffff"
        android:textStyle="bold" />
</RelativeLayout>

Afterwards we include the layout in our MainActivity three times right before our FloatingActionButton.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    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.MainActivity">
    ...
    <LinearLayout
        android:id="@+id/infoMain"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="@color/colorPrimaryDark"
        android:visibility="visible" >

        <include layout="@layout/control_info" android:id="@+id/info1"/>
        <include layout="@layout/control_info" android:id="@+id/info2"/>
        <include layout="@layout/control_info" android:id="@+id/info3" />
    </LinearLayout>

    <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/infoMain"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:layout_margin="16dp"
        android:src="@android:drawable/ic_dialog_map"
        android:onClick="onFloatingButtonClicked"/>
</RelativeLayout>

The implemention of the three NavigationControls looks similar to those of NextManeuverControl. We save our value, unit and description views in the bindView method and update its text in onNavigation. We subclassed our control to show different information for ETA, ETE and distance.The controls will be initialized with static route information. So they will show valid values, even if there is no usable gps signal yet. The initialization takes place in the corresponding onStartNavigation methods. To bridge the gap after calculating the route to the first getGuidanceInformation() call we also update the control with the RouteInformation in void onNavigationStarted(RouteInformation info).

package com.ptvag.navigation.tutorial;

import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.ptvag.navigation.sdk.NavigationInformation;
import com.ptvag.navigation.sdk.NavigationSDK;

import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;

public class NavigationInformationControl implements NavigationControl {
    private ViewGroup main;
    TextView value;
    TextView unit;
    TextView description;

    public static class Distance extends NavigationInformationControl {
        @Override
        public void onNavigationStarted(RouteInformation info) {
            String[] tmp = NavigationSDK.getLocalizedDistanceString(info.getLength(), true, false, "").split(" ");
            value.setText(tmp[0]);
            unit.setText(tmp[1]);
            description.setText("distance");
        }        

        @Override
        public void onNavigation(final NavigationInformation info, final GPSData gps){
            super.onNavigation(info, gps);
            String[] tmp = NavigationSDK.getLocalizedDistanceString(info.getDistToDestination(), true, false, "").split(" ");
            value.setText(tmp[0]);
            unit.setText(tmp[1]);
            description.setText("distance");
        }
    }

    public static class ETA extends NavigationInformationControl {
        @Override
        public void onNavigationStarted(RouteInformation info) {
            SimpleDateFormat format = new SimpleDateFormat("HH:mm");
            value.setText(format.format(info.getEstimatedTimeOfArrival()));
            unit.setText("h");
            description.setText("ETA");
        }

        @Override
        public void onNavigation(final NavigationInformation info, final GPSData gps){
            super.onNavigation(info, gps);
            SimpleDateFormat format = new SimpleDateFormat("HH:mm");
            value.setText(format.format(info.getEstimatedTimeOfArrival()));
            unit.setText("h");
            description.setText("ETA");
        }
    }

    public static class ETE extends NavigationInformationControl {
        @Override
        public void onNavigationStarted(RouteInformation info) {
            value.setText(formatTimeSpan(info.getDuration()));
            unit.setText(formatTimeSpanWithoutValue(info.getDuration()));
            description.setText("ETE");
        }

        @Override
        public void onNavigation(final NavigationInformation info, final GPSData gps){
            super.onNavigation(info, gps);
            value.setText(formatTimeSpan(info.getTimeToDestination()));
            unit.setText(formatTimeSpanWithoutValue(info.getTimeToDestination()));
            description.setText("ETE");
        }
    }

    public void bindView(ViewGroup view) {
        main = view;
        value = (TextView) main.findViewById(R.id.value);
        unit = (TextView) main.findViewById(R.id.unit);
        description = (TextView) main.findViewById(R.id.description);

        main.setVisibility(View.GONE);
    }

    @Override
    public void onGPS(GPSData gps) {
    }

    @Override
    public void onNavigationStarted(RouteInformation info) {
        main.setVisibility(View.VISIBLE);
    }

    @Override
    public void onNavigationStopped() {
        main.setVisibility(View.GONE);
    }

    @Override
    public public void onNavigation(final NavigationInformation info, final GPSData gps) {
    }

    String formatTimeSpan(long totalSeconds) {
        double doubleDays = (double) totalSeconds / (double) (3600 * 24);
        long days = TimeUnit.SECONDS.toDays(totalSeconds);
        long hours = TimeUnit.SECONDS.toHours(totalSeconds) - (days *24);
        long minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) - (TimeUnit.SECONDS.toHours(totalSeconds)* 60);

        if (days >= 1) {
            return String.format("%.1f", doubleDays);
        } else {
            return String.format("%d:%02d", hours, minutes);
        }
    }

    String formatTimeSpanWithoutValue(long totalSeconds) {
        double days = (double) totalSeconds / (double) (3600 * 24);
        if (days > 1d) {
            return "d";
        } else {
            return "h";
        }
    }
}

Finally we need to add this controls in the MainActivity and we are good to go.

...
public class MainActivity extends DrawerActivity implements OnInitializeListener {
    private MapView mapView;
    private NavigationLoop navigationLoop;
    private final NextManeuverControl nextManeuverControl = new NextManeuverControl();
    private final NavigationInformationControl distanceControl = new NavigationInformationControl.Distance();
    private final NavigationInformationControl etaControl = new NavigationInformationControl.ETA();
    private final NavigationInformationControl eteControl = new NavigationInformationControl.ETE();

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        inflateNavigationMenu(R.menu.drawer_menu);

        nextManeuverControl.bindView((ViewGroup)findViewById(R.id.stationInfoMain));
        distanceControl.bindView((ViewGroup)findViewById(R.id.info1));
        etaControl.bindView((ViewGroup)findViewById(R.id.info2));
        eteControl.bindView((ViewGroup)findViewById(R.id.info3));

        ...

        navigationLoop = new NavigationLoop(handler, mapView);
        navigationLoop.addControl(nextManeuverControl);
        navigationLoop.addControl(distanceControl);
        navigationLoop.addControl(etaControl);
        navigationLoop.addControl(eteControl);
        ...
    }
}

Autozoom

Manual zoom levels hide many informations while driving. We need suitable zoom levels according to our current speed and navigation situation. We provide optimized zoom levels while driving if you add a doAutoZoom call to your NavigationLoop.

public class MapView extends com.ptvag.navigation.sdk.MapView implements NavigationControl {
    ...
    @Override
    public void onNavigation(NavigationInformation info, GPSData gps) {
        doAutoZoom(info, gps, true);
        update();
    }
    ...
}

Please have a look at the following NavigationSDK methods to configure the Autozoom:

Config method Description
setAutoZoomSpeedScales() Set the auto zoom scales for the given speeds for tracking and navigation mode
setAutoZoomScaleMargins() Set the auto zoom scale margins in 2d and 3d
setAutoZoomSteppingValues() Set the auto zoom linear approximation stepping values

Adjusting the map in driving direction

For a better overview it is usual to rotate the map in the current driving direction. Herefore we add automatic map rotation to our NavigationLoop and move the FixPoint of our MapView a bit down on the screen to have a better look ahead.

public class MapView extends com.ptvag.navigation.sdk.MapView implements NavigationControl {
    ...
    @Override
    public void onNavigation(NavigationInformation info, GPSData gps) {
        doAutoZoom(info, gps, true);
        setOrientation(270 + gps.getCourse());
        update();
    }
    ...
}

Tunnel extrapolation

While driving through a tunnel we won't get a valid GPS signal and our current navigation will stop in front of the tunnel until we leave it an reacquire a valid GPS signal. We can check if we currently are in a tunnel by calling NavigationSDK.checkIfTunnel(info, gps); . If we are, we replace our current MapView Position an heading with an extrapolated one and activate the gray style of our MapView.

public class MapView extends com.ptvag.navigation.sdk.MapView implements NavigationControl {
    private boolean inTunnel = false;

    ...

    @Override
    public void onGPS(GPSData gps) {
        if (!inTunnel) {
            setGrayMode(false);
            setMapMarker(
                    gps.getGPSPositionMerc(),
                    gps.getCourse(),
                    true,
                    com.ptvag.navigation.sdk.MapView.MapMarkerStyle.Directed
            );

            setCenter(gps.getGPSPositionMerc());
        }
        update();
    }

    @Override
    public void onNavigation(NavigationInformation info, GPSData gps) {
        try {
            inTunnel = NavigationSDK.checkIfTunnel(info, gps);
        } catch (NavigationException e) {
            e.printStackTrace();
        }

        if (inTunnel) {
            setGrayMode(true);
            setMapMarker(
                    gps.getExtraPolatedPos(),
                    gps.getExtraPolatedCourse(),
                    true,
                    com.ptvag.navigation.sdk.MapView.MapMarkerStyle.Directed
            );

            setCenter(gps.getExtraPolatedPos());
            setOrientation(270 + gps.getExtraPolatedCourse());
        } else {
            setOrientation(270 + gps.getCourse());
        }

        doAutoZoom(info, gps, true);
        update();
    }
    ...
}