Time to leave
Purpose
This article shows how to use the TomTom Maps SDK for Android to create an application that calculates the time to leave previous to reaching the destination point at the desired time.

Getting Started
A user can plan their route to reach the destination by using the following:
- Departure and destination locations.
- Travel mode.
- The time they want to reach the destination.
- Optionally preparation time before the departure.
Next:
- The route is calculated and displayed on a map.
- The time to leave timer starts to count down.
- The route is recalculated each minute to check for any traffic delays.
- In case of any changes in traffic, the countdown timer gets updated and the user is notified.
This article does not cover the whole of the application's code description, only the code sections where the TomTom SDK is used.
You can find and clone the working application on our GitHub. Have fun with modifying it and testing our APIs! If you want to follow a step-by-step application creation from scratch, check out the “Search Along the Route” tutorial.
The following sections describe the usage of:
- The TomTom LocationSource API to get the user's current location
- The TomTom Search SDK module to search for addresses and geocode map positions
- The TomTom Map SDK module to display a map
- The TomTom Routing SDK module to plan routes
Prerequisites:
-
Clone a GitHub repository, with the source code of the TimeToLeave application:
git clone https://github.com/tomtom-international/tomtom-use-case-time-to-leave-android.git
- If you don't have an API key visit a How to get a TomTom API key site and create one.
- Create a build config fields with the API keys, which will be used later in the application:
android { compileSdkVersion 29 defaultConfig { buildConfigField("String", "MAPS_API_KEY", "\"YOUR_KEY\"") buildConfigField("String", "ROUTING_API_KEY", "\"YOUR_KEY\"") buildConfigField("String", "SEARCH_API_KEY", "\"YOUR_KEY\"") (...)
Current location
To get the users device location, a LocationSource class is used. This class uses LocationUpdateListener interface to notify about new location updates.
_
import com.tomtom.online.sdk.location.FusedLocationSource;
import com.tomtom.online.sdk.location.LocationSource;
import com.tomtom.online.sdk.location.LocationUpdateListener;
//(...)
public class MainActivity extends AppCompatActivity implements LocationUpdateListener {
private LocationSource locationSource;
//(...)
}
import com.tomtom.online.sdk.location.FusedLocationSource
import com.tomtom.online.sdk.location.LocationSource
import com.tomtom.online.sdk.location.LocationUpdateListener
//(...)
class MainActivity : AppCompatActivity(), LocationUpdateListener {
private lateinit var locationSource: LocationSource
//(...)
}
The application needs to have proper permissions granted which is handled by onRequestPermissionsResult
callback, so that location callbacks can be retrieved successfully. A PermissionChecker
class is used to check whether the application has already assigned proper permissions. If not, then a requestPermissions
function is called so that the user sees a permission request dialog. If location permissions have been granted inside an onRequestPermissionsResult
function, a locationSource.activate()
method is invoked. As a result, the application receives a GPS location in an onLocationChanged
callback function.
_
private void initLocationSource() {
PermissionChecker permissionChecker = AndroidPermissionChecker.createLocationChecker(this);
if(permissionChecker.ifNotAllPermissionGranted()) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_LOCATION);
}
locationSource = new FusedLocationSource(this, LocationRequest.create());
locationSource.addLocationUpdateListener(this);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_LOCATION) {
if (grantResults.length >= 2 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED &&
grantResults[1] == PackageManager.PERMISSION_GRANTED) {
locationSource.activate();
} else {
Toast.makeText(this, R.string.location_permissions_denied, Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onLocationChanged(Location location) {
if (latLngCurrentPosition == null) {
latLngCurrentPosition = new LatLng(location);
locationSource.deactivate();
}
}
private fun initLocationSource() {
val permissionChecker = AndroidPermissionChecker.createLocationChecker(this)
if (permissionChecker.ifNotAllPermissionGranted()) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_REQUEST_LOCATION)
}
locationSource = FusedLocationSource(this, LocationRequest.create())
locationSource.addLocationUpdateListener(this)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_LOCATION) {
if (grantResults.size >= 2 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED &&
grantResults[1] == PackageManager.PERMISSION_GRANTED) {
locationSource.activate()
} else {
Toast.makeText(this, R.string.location_permissions_denied, Toast.LENGTH_SHORT).show()
}
}
}
override fun onLocationChanged(location: Location) {
if (latLngCurrentPosition == null) {
latLngCurrentPosition = LatLng(location)
locationSource.deactivate()
}
}

Address & POI search / Typeahead search
To feed departure and destination autocomplete text fields with the list of suggested positions, Results from the Search API query are used.
-
First, a
searchAPI
object is defined and initialized:_
private SearchApi searchApi; //(...) searchApi = OnlineSearchApi.create(this, BuildConfig.SEARCH_API_KEY);
private lateinit var searchApi: SearchApi //(...) searchApi = OnlineSearchApi.create(this, BuildConfig.SEARCH_API_KEY)
-
Then, a searchAddress function handles points of interest (POI) & address search. It takes two parameters:
- A search word which is used inside a query.
- An
AutoCompleteTextView
object which is used to match whether anafterTextChanged
event has been called from the departure text field instead of the destination field.
_
private void searchAddress(final String searchWord, final AutoCompleteTextView autoCompleteTextView) { searchApi.search(new FuzzySearchQueryBuilder(searchWord) .withLanguage(Locale.getDefault().toLanguageTag()) .withTypeAhead(true) .withMinFuzzyLevel(SEARCH_FUZZY_LVL_MIN).build()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new DisposableSingleObserver<FuzzySearchResponse>() { @Override public void onSuccess(FuzzySearchResponse fuzzySearchResponse) { if (!fuzzySearchResponse.getResults().isEmpty()) { searchAutocompleteList.clear(); searchResultsMap.clear(); if (autoCompleteTextView == atvDepartureLocation && latLngCurrentPosition != null) { String currentLocationTitle = getString(R.string.main_current_position); searchAutocompleteList.add(currentLocationTitle); searchResultsMap.put(currentLocationTitle, latLngCurrentPosition); } for (FuzzySearchResult result : fuzzySearchResponse.getResults()) { String addressString = result.getAddress().getFreeformAddress(); searchAutocompleteList.add(addressString); searchResultsMap.put(addressString, result.getPosition()); } searchAdapter.clear(); searchAdapter.addAll(searchAutocompleteList); searchAdapter.getFilter().filter(""); } } @Override public void onError(Throwable e) { Toast.makeText(MainActivity.this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); } }); }
private fun searchAddress(searchWord: String, autoCompleteTextView: AutoCompleteTextView) { searchApi.search(FuzzySearchQueryBuilder(searchWord) .withLanguage(Locale.getDefault().toLanguageTag()) .withTypeAhead(true) .withMinFuzzyLevel(SEARCH_FUZZY_LVL_MIN).build()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : DisposableSingleObserver<FuzzySearchResponse>() { override fun onSuccess(fuzzySearchResponse: FuzzySearchResponse) { if (!fuzzySearchResponse.results.isEmpty()) { searchAutocompleteList.clear() searchResultsMap.clear() if (autoCompleteTextView === atv_main_departure_location && latLngCurrentPosition != null) { val currentLocationTitle = getString(R.string.main_current_position) searchAutocompleteList.add(currentLocationTitle) searchResultsMap[currentLocationTitle] = latLngCurrentPosition!! } for (result in fuzzySearchResponse.results) { val addressString = result.address.freeformAddress searchAutocompleteList.add(addressString) searchResultsMap[addressString] = result.position } searchAdapter.apply { this.clear() this.addAll(searchAutocompleteList) this.filter.filter("") } } } override fun onError(e: Throwable) { Toast.makeText(this@MainActivity, e.localizedMessage, Toast.LENGTH_SHORT).show() } }) }
-
Inside the
searchAddress
function, asearchApi.search(...)
method is called.-
It takes a
FuzzySearchQuery
object as a parameter where you can provide necessary information like search results language, category, position etc. -
FuzzySearchQueryBuilder
is used to build theFuzzySearchQuery
object. -
The following list of options can set the search query:
withLanguage(Locale.getDefault().toLanguageTag())
- to return the results in default language on the mobile device.withTypeAhead(true))
- to treat the searchWord query as a partial input and search service enters a predictive mode.withMinFuzzyLevel(2))
- to set the fuzziness level to use normal n-gram spell checking. Feel free to experiment with other fuzziness levels.
-
The
search
method from thesearchApi
object, returns aFuzzySearchResponse
observable object.-
When search operation is finished, it emits either a successful value or an error.
-
If a successful value is emitted, a method named onSuccess is executed in the subscribing
DisposableSingleObserver
, otherwise anonError
method is executed. -
The same
searchAddress
function is used in the destination text field control. -
If there are any results in the
FuzzySearchResponse
object and the current location is known, then it is added to the departure autocomplete list of suggestions as a first option. -
Then this list is filled with addresses from the
FuzzySearchResponse
object.
-
-
-
When the departure position is set, the user can choose destination position, arrival time, preparation time and a travel mode. All these parameters are gathered and passed to a
CountDownActivity.prepareIntent
method._
Intent intent = CountdownActivity.prepareIntent( MainActivity.this, latLngDeparture, latLngDestination, travelModeSelected, arrivalTimeInMillis, preparationTimeSelected); startActivity(intent);
val intent = CountdownActivity.prepareIntent( this@MainActivity, latLngDeparture, latLngDestination, travelModeSelected, arrivalTimeInMillis, preparationTimeSelected) startActivity(intent)
TomTom Map display module
A CountDown activity uses map and routing modules from the TomTom Maps SDK. It implements an OnMapReadyCallback
interface, so that an onMapReady
method is called after the TomTom map is ready to be used.
_
public class CountdownActivity extends AppCompatActivity implements OnMapReadyCallback {
private TomtomMap tomtomMap;
private RoutingApi routingApi;
//(...)
@Override
public void onMapReady(@NonNull TomtomMap tomtomMap) {
this.tomtomMap = tomtomMap;
this.tomtomMap.setMyLocationEnabled(true);
this.tomtomMap.clear();
showDialog(dialogInProgress);
requestRoute(departure, destination, travelMode, arriveAt);
}
class CountdownActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var tomtomMap: TomtomMap
private lateinit var routingApi: RoutingApi
//(...)
override fun onMapReady(tomtomMap: TomtomMap) {
this.tomtomMap = tomtomMap
tomtomMap.apply {
this.isMyLocationEnabled = true
this.clear()
}
showDialog(dialogInProgress)
requestRoute(departure, destination, travelMode, arriveAt)
}
<fragment
android:id="@+id/mapFragment"
android:name="com.tomtom.online.sdk.map.MapFragment"
android:layout_width="@dimen/size_none"
android:layout_height="@dimen/size_none"
android:layout_marginTop="@dimen/size_none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_countdown_timer"/>
After the startActivity
method is called inside a CountDownActivity.onCreate(...)
method, TomTom services and all other settings gathered from previous activity are initialized.
_
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_countdown);
initTomTomServices();
initToolbarSettings();
Bundle settings = getIntent().getBundleExtra(BUNDLE_SETTINGS);
initActivitySettings(settings);
}
private void initTomTomServices() {
routingApi = OnlineRoutingApi.create(this, BuildConfig.ROUTING_API_KEY);
Map<ApiKeyType, String> mapKeys = new HashMap<>();
mapKeys.put(ApiKeyType.MAPS_API_KEY, BuildConfig.MAPS_API_KEY);
MapProperties mapProperties = new MapProperties.Builder()
.keys(mapKeys)
.build();
MapFragment mapFragment = MapFragment.newInstance(mapProperties);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.mapFragment, mapFragment)
.commit();
mapFragment.getAsyncMap(this);
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_countdown)
initTomTomServices()
initToolbarSettings()
initActivitySettings(intent.getBundleExtra(BUNDLE_SETTINGS))
}
private fun initTomTomServices() {
routingApi = OnlineRoutingApi.create(applicationContext, BuildConfig.ROUTING_API_KEY)
val mapKeys = mapOf(ApiKeyType.MAPS_API_KEY to BuildConfig.MAPS_API_KEY)
val mapProperties = MapProperties.Builder().keys(mapKeys).build()
val mapFragment = MapFragment.newInstance(mapProperties)
supportFragmentManager
.beginTransaction()
.replace(R.id.mapFragment, mapFragment)
.commit()
mapFragment.getAsyncMap(this)
}
When the map is ready to be used, TomTom SDK automatically invokes an onMapReady
callback method where:
- The
tomtomMap
private field is initialized. - An 'in progress' dialog is displayed.
- A
requestRoute
method is called.

TomTom Routing module
The CountDown activity uses TomTom Routing APIs to perform routing requests and operations like drawing a route on the map or displaying start and end route icons.
-
First, a private
RoutingApi
class member is declared and initialized inside aninitTomTomServices
function which is shown in several previous paragraphs.The
requestRoute
method takes four parameters:LatLng
departure position,LatLng
destination position,TravelMode
which describes which type of transport user choosed,- A
date
which determines expected arrival time.
_
private static final int ONE_MINUTE_IN_MILLIS = 60000; private static final int ROUTE_RECALCULATION_DELAY = ONE_MINUTE_IN_MILLIS; private void requestRoute(final LatLng departure, final LatLng destination, TravelMode byWhat, Date arriveAt) { if (!isInPauseMode) { //(...) } else { timerHandler.removeCallbacks(requestRouteRunnable); timerHandler.postDelayed(requestRouteRunnable, ROUTE_RECALCULATION_DELAY); } }
private const val ONE_MINUTE_IN_MILLIS = 60000 private const val ROUTE_RECALCULATION_DELAY = ONE_MINUTE_IN_MILLIS private fun requestRoute(departure: LatLng?, destination: LatLng?, byWhat: TravelMode?, arriveAt: Date?) { if (!isInPauseMode) { //(...) } else { timerHandler.removeCallbacks(requestRouteRunnable) timerHandler.postDelayed(requestRouteRunnable, ROUTE_RECALCULATION_DELAY.toLong()) } }
There is a check whether the application is not in a pause mode in the first line of the
requestRoute
function.-
If the application is in the pause mode, a
requestRouteRunnable
object is posted in atimerHandler
with a one-minute delay. -
The
RequestRouteRunnable
andTimerHandler
objects are defined as a private members of theCountDownActivity
class:_
private final Handler timerHandler = new Handler(); private final Runnable requestRouteRunnable = new Runnable() { @Override public void run() { requestRoute(departure, destination, travelMode, arriveAt); } };
private val timerHandler = Handler() private val requestRouteRunnable = Runnable { requestRoute(departure, destination, travelMode, arriveAt) }
-
If the application is not in the pause mode, the
RouteSpecification
object needs to be prepared in order to plan a route._
if (!isInPauseMode) { RouteDescriptor routeDescriptor = new RouteDescriptor.Builder() .routeType(RouteType.FASTEST) .considerTraffic(true) .travelMode(byWhat) .build(); RouteCalculationDescriptor routeCalculationDescriptor = new RouteCalculationDescriptor.Builder() .routeDescription(routeDescriptor) .arriveAt(arriveAt) .build(); RouteSpecification routeSpecification = new RouteSpecification.Builder(departure, destination) .routeCalculationDescriptor(routeCalculationDescriptor) .build(); //(...)
if (!isInPauseMode) { val routeDescriptor = RouteDescriptor.Builder() .routeType(com.tomtom.online.sdk.routing.route.description.RouteType.FASTEST) .considerTraffic(true) .travelMode(byWhat!!) .build() val routeCalculationDescriptor = RouteCalculationDescriptor.Builder() .routeDescription(routeDescriptor) .arriveAt(arriveAt!!) .build() val routeSpecification = RouteSpecification.Builder(departure!!, destination!!) .routeCalculationDescriptor(routeCalculationDescriptor) .build() //(...)
-
The
RouteDescriptor
andRouteCalculationDescriptor
are used to construct theRouteSpecification
object:routeType
- to allow the users to select one type of route from: fastest, shortest, thrilling or eco.considerTraffic
- when enabled, current traffic information is used during the route planning, when disabled - only historical traffic data.travelMode
– you can choose one of the following: car, pedestrian, bicycle, truck, other params are listed here.arriveAt
- a date and time of arrival at the destination point. It’s an important method inside the application because it changes a departure date which later is used to calculate time left before the departure.
-
When the
RouteSpecification
object is created, it is passed as a parameter to aplanRoute
function from theroutingApi
object. This function takes as an argument aRouteCallback
object. In case of an error, anonError
method is called, inside this method the current activity is finished. As a result, the application returns to the previous screen._
@Override public void onError(Throwable e) { hideDialog(dialogInProgress); Toast.makeText(CountdownActivity.this, getString(R.string.toast_error_message_cannot_find_route), Toast.LENGTH_LONG).show(); CountdownActivity.this.finish(); }
override fun onError(e: Throwable) { hideDialog(dialogInProgress) Toast.makeText(this@CountdownActivity, getString(R.string.toast_error_message_cannot_find_route), Toast.LENGTH_LONG).show() this@CountdownActivity.finish() }
-
When the
planRoute
function is finished, anonSuccess(@NotNull RoutePlan routePlan)
function is called and proper actions can be executed._
routingApi.planRoute(routeSpecification, new RouteCallback() { @Override public void onSuccess(@NotNull RoutePlan routePlan) { hideDialog(dialogInProgress); if (!routePlan.getRoutes().isEmpty()) { FullRoute fullRoute = routePlan.getRoutes().get(0); int currentTravelTime = fullRoute.getSummary().getTravelTimeInSeconds(); if (previousTravelTime != currentTravelTime) { int travelDifference = previousTravelTime - currentTravelTime; if (previousTravelTime != 0) { showWarningSnackbar(prepareWarningMessage(travelDifference)); } previousTravelTime = currentTravelTime; displayRouteOnMap(fullRoute.getCoordinates()); String departureTimeString = fullRoute.getSummary().getDepartureTime(); setupCountDownTimer(new DateFormatter().formatWithTimeZone(departureTimeString).toDate()); } else { infoSnackbar.show(); } } timerHandler.removeCallbacks(requestRouteRunnable); timerHandler.postDelayed(requestRouteRunnable, ROUTE_RECALCULATION_DELAY); }
routingApi.planRoute(routeSpecification, object : RouteCallback { override fun onSuccess(routePlan: RoutePlan) { hideDialog(dialogInProgress) if (routePlan.routes.isNotEmpty()) { val fullRoute = routePlan.routes.first() val currentTravelTime = fullRoute.summary.travelTimeInSeconds if (previousTravelTime != currentTravelTime) { val travelDifference = previousTravelTime - currentTravelTime if (previousTravelTime != 0) { showWarningSnackbar(prepareWarningMessage(travelDifference)) } previousTravelTime = currentTravelTime displayRouteOnMap(fullRoute.getCoordinates()) val departureTimeString = fullRoute.summary.departureTime setupCountDownTimer(DateFormatter().formatWithTimeZone(departureTimeString).toDate()) } else { infoSnackbar.show() } } timerHandler.removeCallbacks(requestRouteRunnable) timerHandler.postDelayed(requestRouteRunnable, ROUTE_RECALCULATION_DELAY.toLong()) }
-
The most important task in previous code example is to get a current travel time and compare it to the previous one.
-
If travel times are the same, a Snackbar notification is shown to the user informing him, that there are no changes in the route time since the last check.
-
Otherwise a travel time difference is calculated and stored in a
travelDifference
variable. -
Then, a snackbar notification is shown to the user with the calculated travel time difference.
-
-
Next, the current travel time is stored in a
previousTravelTime
variable.-
The route is displayed on the map.
-
A
setupCountDownTimer
function is called
_
private void setupCountDownTimer(Date departure) { if (isCountdownTimerSet()) { countDownTimer.cancel(); } Date now = Calendar.getInstance().getTime(); final int preparationTimeMillis = preparationTime * ONE_MINUTE_IN_MILLIS; long timeToLeave = departure.getTime() - now.getTime(); countDownTimer = new CountDownTimer(timeToLeave, ONE_SECOND_IN_MILLIS) { public void onTick(long millisUntilFinished) { updateCountdownTimerTextViews(millisUntilFinished); if (!isPreparationMode && millisUntilFinished <= preparationTimeMillis) { isPreparationMode = true; setCountdownTimerColor(COUNTDOWN_MODE_PREPARATION); if (!isInPauseMode) { showPreparationInfoDialog(); } } } public void onFinish() { timerHandler.removeCallbacks(requestRouteRunnable); setCountdownTimerColor(COUNTDOWN_MODE_FINISHED); if (!isInPauseMode) { createDialogWithCustomButtons(); } } }.start(); textViewTravelTime.setText(getString(R.string.travel_time_text, formatTimeFromSecondsDisplayWithoutSeconds(previousTravelTime))); }
private fun setupCountDownTimer(departure: Date) { countDownTimer?.cancel() val now = Calendar.getInstance().time val preparationTimeMillis = preparationTime * ONE_MINUTE_IN_MILLIS val timeToLeave = departure.time - now.time countDownTimer = object : CountDownTimer(timeToLeave, ONE_SECOND_IN_MILLIS.toLong()) { override fun onTick(millisUntilFinished: Long) { updateCountdownTimerTextViews(millisUntilFinished) if (!isPreparationMode && millisUntilFinished <= preparationTimeMillis) { isPreparationMode = true setCountdownTimerColor(COUNTDOWN_MODE_PREPARATION) if (!isInPauseMode) { showPreparationInfoDialog() } } } override fun onFinish() { timerHandler.removeCallbacks(requestRouteRunnable) setCountdownTimerColor(COUNTDOWN_MODE_FINISHED) if (!isInPauseMode) { createDialogWithCustomButtons() } } }.start() text_countdown_travel_time.text = getString(R.string.travel_time_text, formatTimeFromSecondsDisplayWithoutSeconds(previousTravelTime.toLong())) }
-
-
The
setupCountDownTimer
function takes the departure date as a parameter. It checks if there is already a countdown timer service already running in the background, and if there is, it cancels it. Next, a current date is stored in acurrentDate
variable and the preparation time taken from the first activity is stored in apreparationTimeMillis
variable. A time difference between the departure time and the current time is stored in atimeToLeave
variable. It is later passed to aCountDownTimer
constructor. -
The
CountDownTimer
class implements two callbacks:- The first callback is called
onTick(long millisUntilFinished)
.- It updates time info and checks whether the application is in a preparation mode.
- If this is the case, the color of the timer text views changes, and a preparation dialog is displayed to the user.
onFinish()
is called by the timer when counting down is completed.- In this function,
requestRouteRunnable
callback is removed from timerHandler’s queue by callingremoveCallbacks
method. - The color of a timer text views is updated again, and final alert dialog window is displayed to the user with nicely designed message that there is a time to leave!
- In this function,
- The first callback is called

Happy coding!
Summary
This tutorial describes the TomTom SDK and APIs used to build a Time To Leave application. Source code can be downloaded from Github.