Skip to content

05 Route calculation

Overview

In this tutorial you will learn how to calculate a route from A to B and how to influence the routing by setting your vehicle parameters.

Base

You can use your results from Tutorial04 as a base for our new project.

Retrieving the destination

In the last tutorial we added a simple activity to search for addresses. To start a navigation to a search result, we first add a FloatingActionButton as a start button.

<android.support.design.widget.CoordinatorLayout>

    ...

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

In onFloatingButtonClicked we then check which of the three search results are set. The best result will be sent to our MainActivity for the route calculation by bundling it to the MainActivity intent.

Attention

We start the activity with the flags Intent.FLAG_ACTIVITY_CLEAR_TOP and Intent.FLAG_ACTIVITY_SINGLE_TOP to reuse the current MainActivity and avoid instantiating a new one!

public void onFloatingButtonClicked(View view)
{
    Intent intent = new Intent(this, MainActivity.class);
    if (mCurrentHNRResult != null) {
        intent.putExtra("destination", mCurrentHNRResult.getPosition());
    } else if (mCurrentStreetResult != null) {
        intent.putExtra("destination", mCurrentStreetResult.getPosition());
    } else if (mCurrentTownResult != null) {
        intent.putExtra("destination", mCurrentTownResult.getPosition());
    }

    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
    startActivity(intent);
}

Now we only need to override the onNewIntent method of the MainActivity to retrieve the route calculation intent and extract the destination from the intent bundle.

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);

    if (!intent.hasExtra("destination"))
        return;

    final Position destination = intent.getParcelableExtra("destination");
}

Calculating a route

Every long lasting operation should be performed in a background thread to avoid stalling the GUI thread and therefore the user interactions. So we put long lasting operations in a seperate thread. The NavigationSDK helps you with a simple worker thread (the SDKJobQueue) where you can queue your long lasting NavigationSDK operations.

Hint

You can easily use your own asynchronous mechanism. But be aware that most of the NavigationSDK functions are currently mutual exclusive and cannot be called concurrently. So try to call only one NavigationSDK method at a time to avoid locks in your application.

So we create a Runnable with all the route calculation operations and push it via SDKJobQueue.getInstance().push() to the SDKJobQueue. To calculate a route from our current position to our given destination, we create a Tour with the current position in the constructor as the start. The destination is added via addStation. Then we call calculateTour.

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);

    if (!intent.hasExtra("destination"))
        return;

    final Position destination = intent.getParcelableExtra("destination");

    Runnable calcRoute = new Runnable() {
        @Override
        public void run() {
            WayPoint start;
            try {
                // set current position as starting point
                start = new WayPoint(GPSManager.getInstance().getCurrentPosition());
            } catch (NavigationException e) {
                // if it fails use Karlsruhe
                start = new WayPoint(934177, 6268747);
            }

            try {
                Tour tour = new Tour(start);

                tour.addStation(new WayPoint(destination));

                CalculateTourResult result =
                        NavigationSDK.calculateTour(tour, null);

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

    // don't forget to remove the extra to avoid handling it twice
    intent.removeExtra("destination");

    SDKJobQueue.getInstance().push(calcRoute);
}

The result for an example route from Karlsruhe to Stuttgart:

Attention

Try to avoid calling other NavigationSDK methods on another thread while the route calculation is running. For example drawing the map on the GUI thread while calculating the route will lock the GUI thread because most NavigationSDK methods are protected by the same mutex and cannot be called in a concurrent way.

To avoid calling other NavigationSDK methods on another thread while the route calculation is running, we push our NavigationLoop and our GPSManager.getInstance().pushLocation() call also to the SDKJobQueue. So we are sure that all recurring NavigationSDK methods are called on the same thread.

public class MainActivity extends AppCompatActivity {
    ...
    private LocationListener locationListener = new LocationListener() {
        @Override
        public void onLocationChanged(final Location location) {
            SDKJobQueue.getInstance().push(new Runnable() {
                @Override
                public void run() {
                    GPSManager.getInstance().pushLocation(location);
                }
            });
        }
        ...
    };
    ...
    TimerTask navigationLoopTrigger = new TimerTask(){
        @Override
        public void run() {
            SDKJobQueue.getInstance().push(navigationLoop);
        }
    };
    ...
}

Retrieving progress information

Users should be notified about the progress of every long lasting operation. Therefore most of the long lasting operations in the NavigationSDK can be called with an Observer to monitor their progress. To monitor the route calculation progress, we implement an Observer as a member of the MainActivity and call calculateTour with it.

Observer calcRouteObserver = new Observer() {
    @Override
    public void onProgress(final int current, final int total, int job) {
    }

    @Override
    public void onFinished(final int error, int index) {
    }
};
CalculateTourResult result = NavigationSDK.calculateTour(tour, calcRouteObserver);

Attention

The onProgress and onFinished callbacks will be called from the thread in which the route calculation was started from. So in our example from the SDKJobQueue thread.

We create a ProgressDialog right before pushing the calcRoute Runnable to the SDKJobQueue.

showRouteCalculationProgressDialog();
SDKJobQueue.getInstance().push(calcRoute);

private void showRouteCalculationProgressDialog() {
    progressDialog = new ProgressDialog(MainActivity.this);
    progressDialog.setTitle(R.string.route_calc_progress_title);
    progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
    progressDialog.setProgress(0);
    progressDialog.setMax(100);
    progressDialog.setCancelable(false);

    progressDialog.show();
}
To update the ProgressDialog we have to bring the execution back to the GUI thread. So we give the MainActivity a Handler member and post our GUI code to it via a Runnable.
...   
Handler handler = new Handler();
ProgressDialog progressDialog;

private final Observer calcRouteObserver = new Observer() {
    @Override
    public void onProgress(final int current, final int total, int job) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (progressDialog != null) {
                    progressDialog.setMax(total);
                    progressDialog.setProgress(current);
                }
            }
        });
    }

    @Override
    public void onFinished(final int error, int index) {
        super.onFinished(error, index);
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (progressDialog != null) {
                    progressDialog.dismiss();
                    progressDialog = null;
                }
            }
        });
    }
};
...

Currently the intent is handled while the MainActivity is initializing, so it is possible that the MainActivity tries to draw itself for the first time while the route calculation is running. This yields in a lock of the GUI thread for the time of the route calculation and most probably leads to a black screen. To avoid this, we delay the route calculation to the point after the MapView is fully drawn for the first time. For this we implement OnInitializeListener in the MainActivity and set it with setOnInitializeListener on the MapView. We can move the intent handling code to an own function called handleIntent and call it in onFirstTimeDrawn. In onNewIntent we just set the current intent via setIntent(intent).

public class MainActivity extends AppCompatActivity implements OnInitializeListener {
    ...

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

        mapView = (MapView)findViewById(R.id.map);
        mapView.setOnInitializeListener(this);

        ...
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
    }

    ...

    void handleIntent(Intent intent) {
        if (!intent.hasExtra("destination"))
            return;

        final Position destination = intent.getParcelableExtra("destination");

        Runnable calcRoute = new Runnable() {
            @Override
            public void run() {
                WayPoint start;
                try {
                    // set current position as starting point
                    start = new WayPoint(GPSManager.getInstance().getCurrentPosition());
                } catch (NavigationException e) {
                    // if it fails use Karlsruhe
                    start = new WayPoint(934177, 6268747);
                }

                try {               
                    Tour tour = new Tour(start);

                    tour.addStation(new WayPoint(destination));

                    CalculateTourResult result =
                            NavigationSDK.calculateTour(tour, null);

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

        // don't forget to remove the extra to avoid handling it twice
        intent.removeExtra("destination");

        showRouteCalculationProgressDialog();
        SDKJobQueue.getInstance().push(calcRoute);
    }

    @Override
    public void onInitialize() {
    }

    @Override
    public void onFirstTimeDrawn() {
        handleIntent(getIntent());
    }
}

RoutingOptions

Loading a vehicle profile

To avoid setting the routing options manually and to get a quick way to set them for a particular vehicle, we use so called profiles. Profiles describe certain standard vehicles like trucks, cars, transporters and so on. In these profiles (which are text files) the values for the relevant routing and lorry options are preset (some of the options are not in the profiles file, but will be adapted internally to the used vehicle). We highly recommend to use the profile loading mechanism which the SDK provides, instead of setting the options manually, especially the speed tables.

Loading a profile is done by calling the method NavigationSDK.loadVehicleProfile(). The function takes as parameter the full path to the profile file to load.

We deliver a set of profiles in a directory called 'profiles' under the data directory of the tutorial app data.

public class Application extends android.app.Application {

    ...

    @Override
    public void onCreate() {
        super.onCreate();

        ...

        File profilePath = new File(getDataPath(), "profiles");
        NavigationSDK.loadVehicleProfile(new File(profilePath, "truck75.dat"));
    }

Manually setting the routing options

Setting the routing options manually is also possible but only recommended for experts. The NavigationSDK brings several methods to influence the result of the route calculation. For example you can avoid toll roads, decide what kind of vehicle you are driving or if you want a routing optimized for short way or time. To get you started, we set a bunch of options in the Application in a new method setDefaultRoutingOptions. By calling NavigationSDK.getRoutingOptions() we retrieve the default RouteOptions, which we can alter to our needs afterwards. The example is only a starting point to play with. Feel free to alter it to your custom wishes.

private void setDefaultRoutingOptions() {
    try
    {
        Result<RouteOptions> result = NavigationSDK.getRoutingOptions();
        RouteOptions options = result.getResult();

        boolean avoidToll = false;

        if (avoidToll) {
            options.setTollFactor(90);
            options.setTruckTollFactor(90);
            options.setUseTollLayer(true);
            options.setUseTruckTollLayer(true);
        } else {
            options.setUseTollLayer(false);
            options.setUseTruckTollLayer(false);
        }

        options.setTimeFactor(80);

        Result<LorryOptions> lorryResult = NavigationSDK.getLorryOptions();
        LorryOptions lorry = lorryResult.getResult();

        int vehicle = 0;

        if (vehicle == 0) {
            options.setLorry(true);
            lorry.setWeight((short)75);
            lorry.setHeight((short)37);
        } else if (vehicle == 1) {
            options.setLorry(true);
        } else {
            options = null;
        }

        options.setRoutingVehicle(RoutingVehicleType.USER);

        short[] speedTableRouteList = {82,75,74,66,60,45,56,51,48,38,35,23,30,30,15};
        short[] speedTableCalc = {84,76,74,66,60,40,50,38,26,22,11,5,5,5,2};

        options.setSpeedTable_RouteList(speedTableRouteList);
        options.setSpeedTable_Calc(speedTableCalc);

        NavigationSDK.setRoutingOptions(options);
        NavigationSDK.setLorryOptions(lorry);
    }
    catch (NavigationException e)
    {
        Log.e("Tutorial", e.getMessage());
    }
}

Avoid GUI interaction while route calculation

To avoid any possible GUI interaction we need a way to deactivate the NavigationLoop while calculating a route.

class NavigationLoop implements Runnable {
    ...

    AtomicBoolean stopped = new AtomicBoolean(false);

    public void start() {
        stopped.set(false);
    }

    public void stop() {
        stopped.set(true);
    }

    @Override
    public void run() {
        if (stopped.get())
            return;

        ...
    }

    ...
}

We stop the NavigationLoop before pushing the calcRoute Runnable to the SDKJobQueue. And restart it in onFinished of our calcRouteObserver.

private final Observer calcRouteObserver = new Observer() {
    ...

        @Override
        public void onFinished(final int error, int index) {
        super.onFinished(error, index);
        handler.post(new Runnable() {
            @Override
            public void run() {
                ...
                    navigationLoop.start();
            }
        });
    }
};
private void handleIntent(Intent intent) {     
    ...
    navigationLoop.stop();
    showRouteCalculationProgressDialog();
    SDKJobQueue.getInstance().push(calcRoute);
}