Search along a route

Overview

This tutorial shows how to use TomTom Maps SDK for Android to create an application that helps a user find points of interest along a planned route.

It shows how to use:

  • The TomTom Map SDK module to display a map, including markers with custom icons and balloons.
  • The TomTom Routing SDK module to plan routes with and without waypoints.
  • The TomTom Search SDK module to search for points of interest (POIs) and to geocode map positions.

An end user can start interacting with the application by planning a route with departure and destination points. One long click on the map sets a departure point. A second long click sets a destination point and draws a route between those two points on the map.

When the route is visible on the map, the user can type a POI name or category into a search field or click on any of the predefined POI category buttons (gas station, restaurant, ATM). The map displays markers for POIs that match the user's request. The user can add one of the displayed POIs to their planned route by clicking the marker on the map and choosing the "Add to my route" button inside the marker balloon that is then displayed. The route is recalculated and redrawn to include the selected point.

Prerequisites:

  1. Create a new project (minimum SDK API 23 – Android 6.0 “Marshmallow”) with an Empty Activity named MainActivity. Make sure that the AndroidX artifacs option is enabled.
  2. In the build.gradle project file, add the TomTom repository to the list of repositories. The TomTom Maps SDK dependencies are downloaded from there.
    1allprojects {
    2 repositories {
    3 google()
    4 jcenter()
    5 maven {
    6 url "https://maven.tomtom.com:8443/nexus/content/repositories/releases/"
    7 }
    8 }
    9}
  3. In the app/build.gradle file, inside the “android” section add support for the Java 1.8 features.
    1android {
    2 compileOptions {
    3 sourceCompatibility JavaVersion.VERSION_1_8
    4 targetCompatibility JavaVersion.VERSION_1_8
    5 }
    6(...)
  4. In the app/build.gradle file, add dependencies to the TomTom Map, Search and Routing SDK modules along with android support libraries.
    1dependencies {
    2 implementation fileTree(dir: 'libs', include: ['*.jar'])
    3 implementation 'androidx.appcompat:appcompat:1.1.0'
    4 implementation 'androidx.core:core:1.2.0'
    5 implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    6 implementation 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
    7 implementation 'androidx.media:media:1.1.0'
    8 implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    9 implementation 'com.tomtom.online:sdk-maps:2.4782'
    10 implementation 'com.tomtom.online:sdk-routing:2.4782'
    11 implementation 'com.tomtom.online:sdk-search:2.4782'
    12 implementation 'com.tomtom.online:sdk-maps-ui-extensions:2.4782'
    13}
  5. If you don't have an API key visit a How to get a TomTom API key site and create one.
  6. Create a build config fields with the API keys, which will be used later in the application:
    1android {
    2 compileSdkVersion 29
    3 defaultConfig {
    4 buildConfigField("String", "MAPS_API_KEY", "\YOUR_KEY\")
    5 buildConfigField("String", "ROUTING_API_KEY", "\YOUR_KEY\")
    6 buildConfigField("String", "SEARCH_API_KEY", "\YOUR_KEY\")
    7 (...)
  7. Create a file named dimens.xml inside your res/values directory. Add the dimension values of you UI components to it.
    1<resources>
    2 <dimen name="size_none">0dp</dimen>
    3</resources>

Map initialization

To initialize a TomTom map, add a com.tomtom.onlinesdk.map.MapFragment fragment into the main ConstraintLayout section of the activity_main.xml file.

1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 tools:context=".MainActivity">
8
9 <fragment
10 android:id="@+id/mapFragment"
11 android:name="com.tomtom.online.sdk.map.MapFragment"
12 android:layout_width="match_parent"
13 android:layout_height="@dimen/size_none"
14 app:layout_constraintBottom_toBottomOf="parent"
15 app:layout_constraintEnd_toEndOf="parent"
16 app:layout_constraintStart_toStartOf="parent"
17 app:layout_constraintTop_toTopOf="parent" />
18
19</androidx.constraintlayout.widget.ConstraintLayout>

You can start adding event handlers to the map only after it is fully initialized. To test if the map is ready, the MainActivity class should implement an OnMapReadyCallback interface.

Then implement an OnMapLongClickListener interface to provide a callback method for long click events on the map.

Add a initTomTomServices method to part of the MainActivity class where Map Display API modules are initialized. At the same time, add a initUIViews method and a setupUIViewListeners method where User Interface (UI) elements are initialized.

JAVA
KOTLIN
1public class MainActivity extends AppCompatActivity implements OnMapReadyCallback,
2 TomtomMapCallback.OnMapLongClickListener {
3
4 @Override
5 protected void onCreate(Bundle savedInstanceState) {
6 super.onCreate(savedInstanceState);
7 setContentView(R.layout.activity_main);
8 initTomTomServices();
9 initUIViews();
10 setupUIViewListeners();
11 }
12
13 @Override
14 public void onMapReady(@NonNull TomtomMap tomtomMap) {}
15
16 @Override
17 public void onMapLongClick(@NonNull LatLng latLng) {}
18
19 private void initTomTomServices() {
20 Map<ApiKeyType, String> mapKeys = new HashMap<>();
21 mapKeys.put(ApiKeyType.MAPS_API_KEY, BuildConfig.MAPS_API_KEY);
22
23 MapProperties mapProperties = new MapProperties.Builder()
24 .keys(mapKeys)
25 .build();
26 MapFragment mapFragment = MapFragment.newInstance(mapProperties);
27 getSupportFragmentManager()
28 .beginTransaction()
29 .replace(R.id.mapFragment, mapFragment)
30 .commit();
31 mapFragment.getAsyncMap(this);
32 }
33
34 private void initUIViews() {}
35 private void setupUIViewListeners() {}
36}
1class MainActivity : AppCompatActivity(), OnMapReadyCallback, OnMapLongClickListener {
2
3 override fun onCreate(savedInstanceState: Bundle?) {
4 super.onCreate(savedInstanceState)
5 setContentView(R.layout.activity_main)
6 initTomTomServices()
7 initUIViews()
8 setupUIViewListeners()
9 disableSearchButtons()
10 }
11
12 override fun onMapReady(tomtomMap: TomtomMap) {
13 }
14
15 override fun onMapLongClick(latLng: LatLng) {
16 }
17
18 private fun initTomTomServices() {
19 val mapKeys = mapOf(
20 ApiKeyType.MAPS_API_KEY to BuildConfig.MAPS_API_KEY
21 )
22 val mapProperties = MapProperties.Builder()
23 .keys(mapKeys)
24 .build()
25 val mapFragment = MapFragment.newInstance(mapProperties)
26 supportFragmentManager
27 .beginTransaction()
28 .replace(R.id.mapFragment, mapFragment)
29 .commit()
30 mapFragment.getAsyncMap(this)
31 }
32
33 private fun initUIViews() {
34 }
35
36 private fun setupUIViewListeners() {
37 }

Run your application. You should see a map.

The TomTom map handles zooming, panning, rotating and double tapping gestures. In this application you need to add additional map interactions including location handling, long map click events, drawing routes and markers.

Add a private field for the TomTom map object inside the MainActivity class:

JAVA
KOTLIN
private TomtomMap tomtomMap;
private lateinit var tomtomMap: TomtomMap

and initialize it in the onMapReady callback.

JAVA
KOTLIN
1 @Override
2 public void onMapReady(@NonNull final TomtomMap tomtomMap) {
3 this.tomtomMap = tomtomMap;
4 this.tomtomMap.setMyLocationEnabled(true);
5 this.tomtomMap.addOnMapLongClickListener(this);
6 this.tomtomMap.getMarkerSettings().setMarkersClustering(true);
7 }
1override fun onMapReady(tomtomMap: TomtomMap) {
2 this.tomtomMap = tomtomMap
3 this.tomtomMap.let {
4 it.isMyLocationEnabled = true
5 it.addOnMapLongClickListener(this)
6 it.markerSettings.setMarkersClustering(true)
7 }
8}

Override the onRequestPermissionsResult method so that permission callbacks from activities are forwarded to the tomtomMap object.

JAVA
KOTLIN
1@Override
2public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
3 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
4 this.tomtomMap.onRequestPermissionsResult(requestCode, permissions, grantResults);
5}
1override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
2 super.onRequestPermissionsResult(requestCode, permissions, grantResults)
3 tomtomMap.onRequestPermissionsResult(requestCode, permissions, grantResults)
4}

After the onMapReady callback is executed from the Maps SDK, the tomtomMap object is ready to be used. Now you can register your own map event listeners.

Drawing a route on the map

Drawing a route on the map requires that the following additional private fields be defined in the MainActivity class:

  • RoutingApi
  • SearchApi (here used for reverse geocoding of a map position to a valid address)
  • Calculated route
  • Departure coordinates
  • Destination coordinates
  • Waypoint coordinates
JAVA
KOTLIN
1private TomtomMap tomtomMap;
2private SearchApi searchApi;
3private RoutingApi routingApi;
4private Route route;
5private LatLng departurePosition;
6private LatLng destinationPosition;
7private LatLng wayPointPosition;
8private Icon departureIcon;
9private Icon destinationIcon;
1private lateinit var tomtomMap: TomtomMap
2private lateinit var searchApi: SearchApi
3private lateinit var routingApi: RoutingApi
4private var route: Route? = null
5private var departurePosition: LatLng? = null
6private var destinationPosition: LatLng? = null
7private var wayPointPosition: LatLng? = null
8private var departureIcon: Icon? = null
9private var destinationIcon: Icon? = null

Then initialize the TomTom Search and Routing services by adding the following to the initTomTomServices method:

JAVA
KOTLIN
searchApi = OnlineSearchApi.create(this, BuildConfig.SEARCH_API_KEY);
routingApi = OnlineRoutingApi.create(this, BuildConfig.ROUTING_API_KEY);
searchApi = OnlineSearchApi.create(this, BuildConfig.SEARCH_API_KEY)
routingApi = OnlineRoutingApi.create(this, BuildConfig.ROUTING_API_KEY)

Initialize icons for departure and destination positions inside the initUIViews method.

JAVA
KOTLIN
departureIcon = Icon.Factory.fromResources(MainActivity.this, R.drawable.ic_map_route_departure);
destinationIcon = Icon.Factory.fromResources(MainActivity.this, R.drawable.ic_map_route_destination);
departureIcon = Icon.Factory.fromResources(this@MainActivity, R.drawable.ic_map_route_departure)
destinationIcon = Icon.Factory.fromResources(this@MainActivity, R.drawable.ic_map_route_destination)

You need a clearMap function, where all the markers and the route are removed from the map.

JAVA
KOTLIN
1private void clearMap() {
2 tomtomMap.clear();
3 departurePosition = null;
4 destinationPosition = null;
5 route = null;
6}
1private fun clearMap() {
2 tomtomMap.clear()
3 departurePosition = null
4 destinationPosition = null
5 route = null
6}

Update the res/values/strings.xml file by adding strings:

<string name="geocode_no_results">No geocoder results. Choose different location and try again.</string>
<string name="api_response_error">API response error: \'%1\'.</string>

Now you can add an implementation to the function that handles long click events on the map.

JAVA
KOTLIN
1@Override
2public void onMapLongClick(@NonNull LatLng latLng) {
3 if (isDeparturePositionSet() && isDestinationPositionSet()) {
4 clearMap();
5 } else {
6 handleLongClick(latLng);
7 }
8}
9
10private void handleLongClick(@NonNull LatLng latLng) {
11 searchApi.reverseGeocoding(new ReverseGeocoderSearchQueryBuilder(latLng.getLatitude(), latLng.getLongitude()).build())
12 .subscribeOn(Schedulers.io())
13 .observeOn(AndroidSchedulers.mainThread())
14 .subscribe(new DisposableSingleObserver<ReverseGeocoderSearchResponse>() {
15 @Override
16 public void onSuccess(ReverseGeocoderSearchResponse response) {
17 processResponse(response);
18 }
19
20 @Override
21 public void onError(Throwable e) {
22 handleApiError(e);
23 }
24
25 private void processResponse(ReverseGeocoderSearchResponse response) {
26 if (response.hasResults()) {
27 processFirstResult(response.getAddresses().get(0).getPosition());
28 }
29 else {
30 Toast.makeText(MainActivity.this, getString(R.string.geocode_no_results), Toast.LENGTH_SHORT).show();
31 }
32 }
33
34 private void processFirstResult(LatLng geocodedPosition) {
35 if (!isDeparturePositionSet()) {
36 setAndDisplayDeparturePosition(geocodedPosition);
37 } else {
38 destinationPosition = geocodedPosition;
39 tomtomMap.removeMarkers();
40 drawRoute(departurePosition, destinationPosition);
41 }
42 }
43
44 private void setAndDisplayDeparturePosition(LatLng geocodedPosition) {
45 departurePosition = geocodedPosition;
46 createMarkerIfNotPresent(departurePosition, departureIcon);
47 }
48 });
49}
50
51private boolean isDestinationPositionSet() {
52 return destinationPosition != null;
53}
54
55private boolean isDeparturePositionSet() {
56 return departurePosition != null;
57}
58
59private void handleApiError(Throwable e) {
60 Toast.makeText(MainActivity.this, getString(R.string.api_response_error, e.getLocalizedMessage()), Toast.LENGTH_LONG).show();
61}
1override fun onMapLongClick(latLng: LatLng) {
2 if (isDeparturePositionSet && isDestinationPositionSet) {
3 clearMap()
4 } else {
5 handleLongClick(latLng)
6 }
7}
8
9private fun handleLongClick(latLng: LatLng) {
10 searchApi.reverseGeocoding(ReverseGeocoderSearchQueryBuilder(latLng.latitude, latLng.longitude).build())
11 .subscribeOn(Schedulers.io())
12 .observeOn(AndroidSchedulers.mainThread())
13 .subscribe(object : DisposableSingleObserver<ReverseGeocoderSearchResponse?>() {
14 override fun onSuccess(response: ReverseGeocoderSearchResponse) {
15 processResponse(response)
16 }
17
18 override fun onError(e: Throwable) {
19 handleApiError(e)
20 }
21
22 private fun processResponse(response: ReverseGeocoderSearchResponse) {
23 if (response.hasResults()) {
24 processFirstResult(response.addresses[0].position)
25 } else {
26 Toast.makeText(this@MainActivity, getString(R.string.geocode_no_results), Toast.LENGTH_SHORT).show()
27 }
28 }
29
30 private fun processFirstResult(geocodedPosition: LatLng) {
31 if (!isDeparturePositionSet) {
32 setAndDisplayDeparturePosition(geocodedPosition)
33 } else {
34 destinationPosition = geocodedPosition
35 tomtomMap.removeMarkers()
36 drawRoute(departurePosition, destinationPosition)
37 }
38 }
39
40 private fun setAndDisplayDeparturePosition(geocodedPosition: LatLng) {
41 departurePosition = geocodedPosition
42 createMarkerIfNotPresent(departurePosition, departureIcon)
43 }
44 })
45}
46
47private val isDestinationPositionSet: Boolean
48 get() {
49 return destinationPosition != null
50 }
51
52private val isDeparturePositionSet: Boolean
53 get() {
54 return departurePosition != null
55 }
56
57private fun handleApiError(e: Throwable) {
58 Toast.makeText(this@MainActivity, getString(R.string.api_response_error, e.localizedMessage), Toast.LENGTH_LONG).show()
59}

The handleLongClick function calls the searchApi's reverseGeocoding method. This method checks if a road can be found in the place where a long click is registered on the map.

Method reverseGeocoding returns a RxJava Single<ReverseGeocoderSearchResponse> object, subscribe on this object with DisposableSingleObserver<ReverseGeocoderSearchResponse>. When the Single is finished, it emits either a single successful value or an error. If a successful value is emitted, a method onSuccess is executed in the subscribing DisposableSingleObserver, otherwise an onError method is executed.

If the reverse geocoding call is successful, the effect of the LongClick depends on context:

  • The first long click on the map sets the departurePosition object.
  • A second long click sets the destinationPosition object and draws a route on the map.
  • A third long click removes any destination and departure markers and the route from the map.

Next add functions to send a route request to the Routing API and to draw a route from the response on the map.

JAVA
KOTLIN
1private RouteCalculationDescriptor createRouteCalculationDescriptor(RouteDescriptor routeDescriptor, LatLng[] wayPoints) {
2 return (wayPoints != null) ?
3 new RouteCalculationDescriptor.Builder()
4 .routeDescription(routeDescriptor)
5 .waypoints(asList(wayPoints)).build() :
6 new RouteCalculationDescriptor.Builder()
7 .routeDescription(routeDescriptor).build();
8}
9
10private void drawRoute(LatLng start, LatLng stop) {
11 wayPointPosition = null;
12 drawRouteWithWayPoints(start, stop, null);
13}
14
15private RouteSpecification createRouteSpecification(LatLng start, LatLng stop, LatLng[] wayPoints) {
16 RouteDescriptor routeDescriptor = new RouteDescriptor.Builder()
17 .routeType(RouteType.FASTEST)
18 .build();
19 RouteCalculationDescriptor routeCalculationDescriptor = createRouteCalculationDescriptor(routeDescriptor, wayPoints);
20 return new RouteSpecification.Builder(start, stop)
21 .routeCalculationDescriptor(routeCalculationDescriptor)
22 .build();
23}
24
25private void drawRouteWithWayPoints(LatLng start, LatLng stop, LatLng[] wayPoints) {
26 RouteSpecification routeSpecification = createRouteSpecification(start, stop, wayPoints);
27 showDialogInProgress();
28 routingApi.planRoute(routeSpecification, new RouteCallback() {
29 @Override
30 public void onSuccess(@NotNull RoutePlan routePlan) {
31 displayRoutes(routePlan.getRoutes());
32 tomtomMap.displayRoutesOverview();
33 }
34
35 @Override
36 public void onError(@NotNull RoutingException e) {
37 handleApiError(e);
38 clearMap();
39 }
40
41 private void displayRoutes(List routes) {
42 for (FullRoute fullRoute : routes) {
43 route = tomtomMap.addRoute(new RouteBuilder(
44 fullRoute.getCoordinates()).startIcon(departureIcon).endIcon(destinationIcon));
45 }
46 }
47 });
48}
1private fun createRouteCalculationDescriptor(routeDescriptor: RouteDescriptor, wayPoints: Array<LatLng>?): RouteCalculationDescriptor? {
2 return if (wayPoints != null) RouteCalculationDescriptor.Builder()
3 .routeDescription(routeDescriptor)
4 .waypoints(listOf(*wayPoints)).build()
5 else RouteCalculationDescriptor.Builder()
6 .routeDescription(routeDescriptor).build()
7}
8
9private fun drawRoute(start: LatLng?, stop: LatLng?) {
10 wayPointPosition = null
11 drawRouteWithWayPoints(start, stop, null)
12}
13
14private fun createRouteSpecification(start: LatLng, stop: LatLng, wayPoints: Array<LatLng>?): RouteSpecification? {
15 val routeDescriptor = RouteDescriptor.Builder()
16 .routeType(com.tomtom.online.sdk.routing.route.description.RouteType.FASTEST)
17 .build()
18 val routeCalculationDescriptor = createRouteCalculationDescriptor(routeDescriptor, wayPoints)
19 return RouteSpecification.Builder(start, stop)
20 .routeCalculationDescriptor(routeCalculationDescriptor!!)
21 .build()
22}
23
24private fun drawRouteWithWayPoints(start: LatLng?, stop: LatLng?, wayPoints: Array<LatLng>?) {
25 val routeSpecification = createRouteSpecification(start!!, stop!!, wayPoints)
26 showDialogInProgress()
27
28 routingApi.planRoute(routeSpecification!!, object : RouteCallback {
29 override fun onSuccess(routePlan: RoutePlan) {
30 dismissDialogInProgress()
31 displayRoutes(routePlan.routes)
32 tomtomMap.displayRoutesOverview()
33 }
34
35 override fun onError(e: RoutingException) {
36 handleApiError(e)
37 clearMap()
38 }
39
40 private fun displayRoutes(routes: List<FullRoute>) {
41 for (fullRoute in routes) {
42 route = tomtomMap.addRoute(RouteBuilder(
43 fullRoute.getCoordinates()).startIcon(departureIcon).endIcon(destinationIcon))
44 }
45 }
46 })
47}

The createRouteCalculationDescriptor method returns a RouteCalculationDescriptor object:

  • With additional waypoints (if the wayPoints array is not equal to null).
  • Without additional waypoints (if the wayPoints array is equal to null).

The createRouteSpecification method is using internally the createRouteCalculationDescriptor method result and returns a RouteSpecification object.

The drawRouteWithWaypoints method calls the Routing API. If the response is successful, a route is drawn on the map. If the API returns an error, a message is displayed on the screen.

Add a createMarkerIfNotPresent method to display a departure position marker if the destination position is not set:

JAVA
KOTLIN
1private void createMarkerIfNotPresent(LatLng position, Icon icon) {
2 Optional<Marker> optionalMarker = tomtomMap.findMarkerByPosition(position);
3 if (!optionalMarker.isPresent()) {
4 tomtomMap.addMarker(new MarkerBuilder(position)
5 .icon(icon));
6 }
7}
1private fun createMarkerIfNotPresent(position: LatLng?, icon: Icon?) {
2 val optionalMarker: Optional<Marker> = tomtomMap.findMarkerByPosition(position)
3 if (!optionalMarker.isPresent) {
4 tomtomMap.addMarker(MarkerBuilder((position)!!)
5 .icon(icon))
6 }
7}

Now you can draw the route on the map by using long clicks in chosen locations.

Searching for POIs along the route

Add strings to res/values/strings.xml

1<string name="poi_name_key">poiName</string>
2<string name="address_key">address</string>
3<string name="poisearch_hint">Kind of place to add to your route</string>
4<string name="no_search_results">No search results for \'%1\'. Please try another search term.</string>
5<string name="add_to_my_route">Add to my route</string>

Add dimensions to res/values/dimens.xml

1<dimen name="layout_height_xlarge">60dp</dimen>
2<dimen name="layout_height_xxlarge">80dp</dimen>
3<dimen name="spacing_xtiny">5dp</dimen>
4<dimen name="spacing_xsmall">8dp</dimen>
5<dimen name="spacing_small">10dp</dimen>
6<dimen name="text_size_small">12sp</dimen>
7<dimen name="text_size_normal">14sp</dimen>

Add colors to res/values/colors.xml

<color name="bg_balloon_button_color">#C3D552</color>

Use the searchApi object that was created earlier to search for a POI to add to the existing route. Modify the activity_main.xml layout file by adding a search field and its button.

1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 tools:context=".MainActivity">
8
9 <fragment
10 android:id="@+id/mapFragment"
11 android:name="com.tomtom.online.sdk.map.MapFragment"
12 android:layout_width="match_parent"
13 android:layout_height="@dimen/size_none"
14 app:layout_constraintBottom_toTopOf="@+id/layout_edittext"
15 app:layout_constraintEnd_toEndOf="parent"
16 app:layout_constraintStart_toStartOf="parent"
17 app:layout_constraintTop_toTopOf="parent" />
18
19 <androidx.constraintlayout.widget.ConstraintLayout
20 android:id="@+id/layout_edittext"
21 android:layout_width="@dimen/size_none"
22 android:layout_height="@dimen/layout_height_xlarge"
23 android:background="@color/white_fully_opaque"
24 android:paddingBottom="@dimen/spacing_small"
25 app:layout_constraintBottom_toBottomOf="parent"
26 app:layout_constraintEnd_toEndOf="parent"
27 app:layout_constraintStart_toStartOf="parent">
28
29 <EditText
30 android:id="@+id/edittext_main_poisearch"
31 android:layout_width="@dimen/size_none"
32 android:layout_height="@dimen/size_none"
33 android:layout_marginStart="@dimen/spacing_small"
34 android:background="@android:color/transparent"
35 android:hint="@string/poisearch_hint"
36 android:imeOptions="actionSearch"
37 android:textSize="@dimen/text_size_normal"
38 app:layout_constraintBottom_toBottomOf="parent"
39 app:layout_constraintEnd_toStartOf="@+id/btn_main_poisearch"
40 app:layout_constraintStart_toStartOf="parent"
41 app:layout_constraintTop_toTopOf="parent"
42 android:inputType="text" />
43
44 <ImageButton
45 android:id="@+id/btn_main_poisearch"
46 android:layout_width="wrap_content"
47 android:layout_height="@dimen/size_none"
48 android:layout_marginEnd="@dimen/spacing_small"
49 android:layout_marginTop="@dimen/spacing_xtiny"
50 android:adjustViewBounds="true"
51 android:background="@android:color/transparent"
52 app:layout_constraintEnd_toEndOf="parent"
53 app:layout_constraintTop_toTopOf="parent"
54 app:srcCompat="@android:drawable/ic_menu_search" />
55 </androidx.constraintlayout.widget.ConstraintLayout>
56</androidx.constraintlayout.widget.ConstraintLayout>

Then add behaviors to the newly created controls. Add private fields for the search button and text field in the MainActivity class

private ImageButton btnSearch;
private EditText editTextPois;

and initialize them in the initUIViews method.

1btnSearch = findViewById(R.id.btn_main_poisearch);
2editTextPois = findViewById(R.id.edittext_main_poisearch);
3//we are using Kotlin Android Extensions

After initializing the search button, add a listener inside the setupUIViewListeners method to receive events when the button is clicked.

View.OnClickListener searchButtonListener = getSearchButtonListener();
btnSearch.setOnClickListener(searchButtonListener);
btn_main_poisearch.setOnClickListener(searchButtonListener)

Add a method to create a search button listener.

JAVA
KOTLIN
1@NonNull
2private View.OnClickListener getSearchButtonListener() {
3 return new View.OnClickListener() {
4 @Override
5 public void onClick(View v) {
6 handleSearchClick(v);
7 }
8
9 private void handleSearchClick(View v) {
10 if (isRouteSet()) {
11 Optional<CharSequence> description = Optional.fromNullable(v.getContentDescription());
12
13 if (description.isPresent()) {
14 editTextPois.setText(description.get());
15 v.setSelected(true);
16 }
17 if (isWayPointPositionSet()) {
18 tomtomMap.clear();
19 drawRoute(departurePosition, destinationPosition);
20 }
21 String textToSearch = editTextPois.getText().toString();
22 if (!textToSearch.isEmpty()) {
23 tomtomMap.removeMarkers();
24 searchAlongTheRoute(route, textToSearch);
25 }
26 }
27 }
28
29 private boolean isRouteSet() {
30 return route != null;
31 }
32
33 private boolean isWayPointPositionSet() {
34 return wayPointPosition != null;
35 }
36 private void searchAlongTheRoute(Route route, final String textToSearch) {
37 final Integer MAX_DETOUR_TIME = 1000;
38 final Integer QUERY_LIMIT = 10;
39
40 searchApi.alongRouteSearch(new AlongRouteSearchQueryBuilder(textToSearch, route.getCoordinates(), MAX_DETOUR_TIME).withLimit(QUERY_LIMIT).build()
41 .subscribeOn(Schedulers.io())
42 .observeOn(AndroidSchedulers.mainThread())
43 .subscribe(new DisposableSingleObserver<AlongRouteSearchResponse>() {
44 @Override
45 public void onSuccess(AlongRouteSearchResponse response) {
46 displaySearchResults(response.getResults());
47 }
48
49 private void displaySearchResults(List<AlongRouteSearchResult> results) {
50 if (!results.isEmpty()) {
51 for (AlongRouteSearchResult result : results) {
52 createAndDisplayCustomMarker(result.getPosition(), result);
53 }
54 tomtomMap.zoomToAllMarkers();
55 } else {
56 Toast.makeText(MainActivity.this, String.format(getString(R.string.no_search_results), textToSearch), Toast.LENGTH_LONG).show();
57 }
58 }
59
60 private void createAndDisplayCustomMarker(LatLng position, AlongRouteSearchResult result) {
61 String address = result.getAddress().getFreeformAddress();
62 String poiName = result.getPoi().getName();
63
64 BaseMarkerBalloon markerBalloonData = new BaseMarkerBalloon();
65 markerBalloonData.addProperty(getString(R.string.poi_name_key), poiName);
66 markerBalloonData.addProperty(getString(R.string.address_key), address);
67
68 MarkerBuilder markerBuilder = new MarkerBuilder(position)
69 .markerBalloon(markerBalloonData)
70 .shouldCluster(true);
71 tomtomMap.addMarker(markerBuilder);
72 }
73
74 @Override
75 public void onError(Throwable e) {
76 handleApiError(e);
77 }
78 });
79 }
80 };
81}
1private val searchButtonListener: View.OnClickListener
2get() {
3 return object : View.OnClickListener {
4 override fun onClick(v: View) {
5 handleSearchClick(v)
6 }
7
8 private fun handleSearchClick(v: View) {
9 if (isRouteSet) {
10 val description: Optional<CharSequence> = Optional.fromNullable(v.contentDescription)
11 if (description.isPresent) {
12 edittext_main_poisearch.setText(description.get())
13 deselectShortcutButtons()
14 v.isSelected = true
15 }
16 if (isWayPointPositionSet) {
17 tomtomMap.clear()
18 drawRoute(departurePosition, destinationPosition)
19 }
20 val textToSearch: String = edittext_main_poisearch.text.toString()
21 if (textToSearch.isNotEmpty()) {
22 tomtomMap.removeMarkers()
23 searchAlongTheRoute(route, textToSearch)
24 }
25 }
26 }
27
28 private val isRouteSet: Boolean
29 get() {
30 return route != null
31 }
32
33 private val isWayPointPositionSet: Boolean
34 get() {
35 return wayPointPosition != null
36 }
37
38 private fun searchAlongTheRoute(route: Route?, textToSearch: String) {
39 val maxDetourTime = 1000
40 val queryLimit = 10
41 disableSearchButtons()
42 showDialogInProgress()
43 searchApi.alongRouteSearch(AlongRouteSearchQueryBuilder(textToSearch, route!!.coordinates, maxDetourTime)
44 .withLimit(queryLimit)
45 .build())
46 .subscribeOn(Schedulers.io())
47 .observeOn(AndroidSchedulers.mainThread())
48 .subscribe(object : DisposableSingleObserver<AlongRouteSearchResponse?>() {
49 override fun onSuccess(response: AlongRouteSearchResponse) {
50 displaySearchResults(response.results)
51 dismissDialogInProgress()
52 enableSearchButtons()
53 }
54
55 private fun displaySearchResults(results: List<AlongRouteSearchResult>) {
56 if (results.isNotEmpty()) {
57 for (result: AlongRouteSearchResult in results) {
58 createAndDisplayCustomMarker(result.position, result)
59 }
60 tomtomMap.zoomToAllMarkers()
61 } else {
62 Toast.makeText(this@MainActivity, String.format(getString(R.string.no_search_results), textToSearch), Toast.LENGTH_LONG).show()
63 }
64 }
65
66 private fun createAndDisplayCustomMarker(position: LatLng, result: AlongRouteSearchResult) {
67 val address: String = result.address.freeformAddress
68 val poiName: String = result.poi.name
69 val markerBalloonData = BaseMarkerBalloon().apply {
70 this.addProperty(getString(R.string.poi_name_key), poiName)
71 this.addProperty(getString(R.string.address_key), address)
72 }
73 val markerBuilder: MarkerBuilder = MarkerBuilder(position)
74 .markerBalloon(markerBalloonData)
75 .shouldCluster(true)
76 tomtomMap.addMarker(markerBuilder)
77 }
78
79 override fun onError(e: Throwable) {
80 handleApiError(e)
81 enableSearchButtons()
82 }
83 })
84 }
85 }
86}

The getSearchButtonListener method creates a View.OnClickListener object. The searchAlongTheRoute function in this object executes a search query for the provided search term along the displayed route. The createAndDisplayCustomMarker method then adds a map marker in the position returned by the search query, including the name and address of the POI.

Adding custom marker balloons

Add a file with a rounded button shape named bg_balloon_button.xml to the res/drawables folder. This adds the styling to the buttons in the marker balloons.

1<?xml version="1.0" encoding="utf-8"?>
2<shape xmlns:android="http://schemas.android.com/apk/res/android"
3 android:shape="rectangle" android:padding="10dp" >
4 <solid android:color="@color/bg_balloon_button_color" />
5 <corners android:radius="20dp" />
6</shape>

To create a custom layout, add a file named marker_custom_balloon.xml inside the res/layout directory.

1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:layout_width="wrap_content"
5 android:layout_height="wrap_content"
6 android:background="@android:color/white">
7
8 <Button
9 android:id="@+id/btn_balloon_waypoint"
10 android:layout_width="wrap_content"
11 android:layout_height="wrap_content"
12 android:layout_marginBottom="@dimen/spacing_small"
13 android:layout_marginEnd="@dimen/spacing_small"
14 android:layout_marginStart="@dimen/spacing_small"
15 android:layout_marginTop="@dimen/spacing_small"
16 android:background="@drawable/bg_balloon_button"
17 android:paddingBottom="@dimen/spacing_xtiny"
18 android:paddingLeft="@dimen/spacing_small"
19 android:paddingRight="@dimen/spacing_small"
20 android:paddingTop="@dimen/spacing_xtiny"
21 android:text="@string/add_to_my_route"
22 android:textAlignment="center"
23 android:textSize="@dimen/text_size_small"
24 android:textStyle="bold"
25 app:layout_constraintBottom_toBottomOf="parent"
26 app:layout_constraintEnd_toEndOf="parent"
27 app:layout_constraintStart_toStartOf="parent"
28 app:layout_constraintTop_toBottomOf="@+id/textview_balloon_poiaddress" />
29
30 <TextView
31 android:id="@+id/textview_balloon_poiname"
32 android:layout_width="wrap_content"
33 android:layout_height="wrap_content"
34 android:layout_marginEnd="@dimen/spacing_xsmall"
35 android:layout_marginStart="@dimen/spacing_xsmall"
36 android:layout_marginTop="@dimen/spacing_xsmall"
37 android:textStyle="bold"
38 app:layout_constraintEnd_toEndOf="parent"
39 app:layout_constraintStart_toStartOf="parent"
40 app:layout_constraintTop_toTopOf="parent" />
41
42 <TextView
43 android:id="@+id/textview_balloon_poiaddress"
44 android:layout_width="wrap_content"
45 android:layout_height="wrap_content"
46 android:layout_marginEnd="@dimen/spacing_xsmall"
47 android:layout_marginStart="@dimen/spacing_xsmall"
48 app:layout_constraintEnd_toEndOf="parent"
49 app:layout_constraintStart_toStartOf="parent"
50 app:layout_constraintTop_toBottomOf="@id/textview_balloon_poiname" />
51</androidx.constraintlayout.widget.ConstraintLayout>

The marker_custom_balloon layout displays a balloon above a map marker that the user has clicked. This balloon has two text fields to display an address and a POI name are displayed, plus a button to add the POI to the existing route.

Finally, add a method createCustomViewAdapter that returns a custom SingleLayoutBalloonViewAdapter adapter object.

Use the marker_custom_balloon layout as an argument for the adapter object. Then override the onBindView method inside the adapter to fill in the marker balloon layout fields. Then implement a btnAddWayPoint onClick event inside the adapter. This events executes a setWayPoint method and recalculates the route to include the marker that the user has clicked.

JAVA
KOTLIN
1private SingleLayoutBalloonViewAdapter createCustomViewAdapter() {
2 return new SingleLayoutBalloonViewAdapter(R.layout.marker_custom_balloon) {
3 @Override
4 public void onBindView(View view, final Marker marker, BaseMarkerBalloon baseMarkerBalloon) {
5 Button btnAddWayPoint = view.findViewById(R.id.btn_balloon_waypoint);
6 TextView textViewPoiName = view.findViewById(R.id.textview_balloon_poiname);
7 TextView textViewPoiAddress = view.findViewById(R.id.textview_balloon_poiaddress);
8 textViewPoiName.setText(baseMarkerBalloon.getStringProperty(getApplicationContext().getString(R.string.poi_name_key)));
9 textViewPoiAddress.setText(baseMarkerBalloon.getStringProperty(getApplicationContext().getString(R.string.address_key)));
10 btnAddWayPoint.setOnClickListener(new View.OnClickListener() {
11 @Override
12 public void onClick(View v) {
13 setWayPoint(marker);
14 }
15
16 private void setWayPoint(Marker marker) {
17 wayPointPosition = marker.getPosition();
18 tomtomMap.clearRoute();
19 drawRouteWithWayPoints(departurePosition, destinationPosition, new LatLng[] {wayPointPosition});
20 marker.deselect();
21 }
22 });
23 }
24 };
25}
1private fun createCustomViewAdapter(): SingleLayoutBalloonViewAdapter {
2 return object : SingleLayoutBalloonViewAdapter(R.layout.marker_custom_balloon) {
3 override fun onBindView(view: View, marker: Marker, baseMarkerBalloon: BaseMarkerBalloon) {
4 textview_balloon_poiname.text = baseMarkerBalloon.getStringProperty(applicationContext.getString(R.string.poi_name_key))
5 textview_balloon_poiaddress.text = baseMarkerBalloon.getStringProperty(applicationContext.getString(R.string.address_key))
6 btn_balloon_waypoint.setOnClickListener(object : View.OnClickListener {
7 override fun onClick(v: View) {
8 setWayPoint(marker)
9 }
10
11 private fun setWayPoint(marker: Marker) {
12 wayPointPosition = marker.position
13 tomtomMap.clearRoute()
14 drawRouteWithWayPoints(departurePosition, destinationPosition, arrayOf(wayPointPosition!!))
15 marker.deselect()
16 }
17 })
18 }
19 }
20}

Use above method inside the onMapReady function to set a marker balloon view adapter.

JAVA
KOTLIN
this.tomtomMap.getMarkerSettings().setMarkerBalloonViewAdapter(createCustomViewAdapter());
it.markerSettings.markerBalloonViewAdapter = createCustomViewAdapter()

Now you should have a fully working application where you can:

  • Display a map.
  • Create a route between 2 points.
  • Display points of interest.
  • Add a single POI to your route.

The additional styling, shortcut buttons, and help screen in the application screenshots are not a part of this tutorial. You can find them, along with all the icons and images used in this tutorial, in the application posted on github.

Summary

This tutorial explained how to create a sample application that searches for and displays points of interest along a route, then replans the route to include one of those POIs.

This application can be extended with other TomTom Maps SDK functions, such as displaying information about traffic and travel distances.

Happy coding!

Example application

The full application, including additional layout changes and improvements, is visible below. It uses a ConstraintLayout with a search field and a button for its main layout. At the bottom of the screen there are also three optional buttons that can be used for quick searches for gas stations, restaurants, and ATMs. There is a help button in the top right corner along with a clear button to remove the route and any markers from the map.