Overview

You will learn: how to find the optimal route and directions for multiple stops with the ArcGIS Route service.

The ArcGIS Routing and Network Analytics Services can find routes, get driving directions, calculate drive times, and solve complicated multiple vehicle routing problems (VRP). If you would like to create an application that can find driving directions and create an optimized route, you can use a route task and the solveRouteAsync method. You provide the "stop" locations, and the service will return a route with directions. Once you have the results you can add the route to a map, display the turn-by-turn directions, or integrate them further into your application. To learn more about the capabilities of the underlying service, please visit the documentation.

In this tutorial you will learn how to use a routing service to calculate an optimal route between two places. You will create a start and a finish point by tapping on the map.

Before you begin

Make sure you have installed the latest version of Android Studio.

Reuse the starter project

If you have completed the Create a starter app tutorial, then copy the project into a new empty folder. Otherwise, download and unzip the project solution. Open, run, and verify the map displays in the device simulator.

Complete or review tutorials Display point, line, and polygon graphics and Access services with OAuth 2.0 as you will repeat some parts of those tutorials here.

Steps

Add a method for handling user input

  1. Your app will track the start and end points for the route and display the route using a GraphicsOverlay. Add members for this in the file app > java > {your.package.name} > MainActivity.java:

    private GraphicsOverlay mGraphicsOverlay;
    private Point mStart;
    private Point mEnd;
    
  2. Create a new member function named createGraphicsOverlay. In it, create a GraphicsOverlay object, assign it to the member variable, and add it to the map view.

    private void createGraphicsOverlay() {
        mGraphicsOverlay = new GraphicsOverlay();
        mMapView.getGraphicsOverlays().add(mGraphicsOverlay);
    }
    
  3. Add new functions to create marker symbols representing the start and end locations of the route request from user tap gestures.

    private void setMapMarker(Point location, SimpleMarkerSymbol.Style style, int markerColor, int outlineColor) {
        float markerSize = 8.0f;
        float markerOutlineThickness = 2.0f;
        SimpleMarkerSymbol pointSymbol = new SimpleMarkerSymbol(style, markerColor, markerSize);
        pointSymbol.setOutline(new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, outlineColor, markerOutlineThickness));
        Graphic pointGraphic = new Graphic(location, pointSymbol);
        mGraphicsOverlay.getGraphics().add(pointGraphic);
    }
    
    private void setStartMarker(Point location) {
        mGraphicsOverlay.getGraphics().clear();
        setMapMarker(location, SimpleMarkerSymbol.Style.DIAMOND, Color.rgb(226, 119, 40), Color.BLUE);
        mStart = location;
        mEnd = null;
    }
    
    private void setEndMarker(Point location) {
        setMapMarker(location, SimpleMarkerSymbol.Style.SQUARE, Color.rgb(40, 119, 226), Color.RED);
        mEnd = location;
        // findRoute();
    }
    
  4. Add a new method to the MainActivity to interpret user input when the map is clicked or touched by a user gesture and call the symbol display functions you coded in the prior step:

    private void mapClicked(Point location) {
        if (mStart == null) {
            // Start is not set, set it to a tapped location
            setStartMarker(location);
        } else if (mEnd == null) {
            // End is not set, set it to the tapped location then find the route
            setEndMarker(location);
        } else {
            // Both locations are set; re-set the start to the tapped location
            setStartMarker(location);
        }
    }
    

Add a touch event handler to the map view

  1. Update your app's setupMap() method to add an OnTouchListener to listen for user touch events on the MapView and connect it to the mapClicked() method:

            mMapView.setMap(map);
    
            // *** ADD ***
            mMapView.setOnTouchListener(new DefaultMapViewOnTouchListener(this, mMapView) {
                @Override public boolean onSingleTapConfirmed(MotionEvent e) {
                    android.graphics.Point screenPoint = new android.graphics.Point(
                            Math.round(e.getX()),
                            Math.round(e.getY()));
                    Point mapPoint = mMapView.screenToLocation(screenPoint);
                    mapClicked(mapPoint);
                    return super.onSingleTapConfirmed(e);
                }
            });
    
  2. Call the createGraphicsOverlay() method from onCreate() immediately after calling setupMap():

    setupMap();
    // *** ADD ***
    createGraphicsOverlay();
    

At this point you can run the app and test the display of the marker symbols on the map. Check that tapping on the map adds a point marker, and tapping a second time adds a second marker. Tapping a third time should delete both existing markers and start again by adding the first marker in the new location.

Add authentication

  1. Using the ArcGIS routing services requires an authenticated user and a valid token. The Android SDK handles this for you with AuthenticationManager. Create an app_settings.xml resource file to hold your client ID and custom redirect URL, or reuse the one created in Access services with OAuth 2.0. Also add here the URL to the routing service.

    1. In the Project view, right-click app and choose New > Android resource file.
    2. Set the File name to app_settings.xml.
    3. In Resource type choose Values.
    4. Leave the other options as default (Source set as main, Directory name as values) and click OK.
    5. In the new values file you created, add the following string elements:
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
      <!-- *** ADD *** -->
      <string name="redirect_url">my-devlab-app://auth</string>
      <string name="client_id">YOUR_CLIENT_ID</string>
      <string name="routing_url">https://route.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World</string>
    
    • Replace the client_id and redirect_url values with the values shown on the authentication tab of your application definition.

Set the default OAuth Intent Receiver

  1. Update the android manifest to indicate the Activity and intent filter to use that will handle the redirect URL. The Android manifest file is located in the project hierarchy under app > manifests > AndroidManifest.xml. Add the following <activity> tag, taking note of the scheme usage of your redirect URL:

    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <!-- *** ADD *** -->
    <activity
        android:name="com.esri.arcgisruntime.security.DefaultOAuthIntentReceiver"
        android:label="OAuthIntentReceiver"
        android:launchMode="singleTask">
        <intent-filter>
            <action android:name="android.intent.action.VIEW"/>
            <category android:name="android.intent.category.DEFAULT"/>
            <category android:name="android.intent.category.BROWSABLE"/>
            <data android:scheme="my-devlab-app"/>
        </intent-filter>
    </activity>
    

Make sure the scheme matches exactly to the value entered in Redirect URIs of your application definition.

The next part of the project is to write the code to configure a DefaultAuthenticationChallengeHandler for OAuth 2.0.

Integrate OAuth 2.0 into your app

  1. The DefaultAuthenticationChallengeHandler handles all security types, including OAuth 2.0 if you add an OAuthConfiguration to the AuthenticationManager. In the file app > java > {your.package.name} > MainActivity.java, create a new member function named setupOAuthManager() to setup the authentication manager. This code creates a configuration with the parameters you assigned to your app in app_settings.xml and then assigns that configuration to the AuthenticationManager.

    private void setupOAuthManager() {
        String clientId = getResources().getString(R.string.client_id);
        String redirectUrl = getResources().getString(R.string.redirect_url);
    
        try {
            OAuthConfiguration oAuthConfiguration = new OAuthConfiguration("https://www.arcgis.com", clientId, redirectUrl);
            DefaultAuthenticationChallengeHandler authenticationChallengeHandler = new DefaultAuthenticationChallengeHandler(this);
            AuthenticationManager.setAuthenticationChallengeHandler(authenticationChallengeHandler);
            AuthenticationManager.addOAuthConfiguration(oAuthConfiguration);
        } catch (MalformedURLException e) {
            showError(e.getMessage());
        }
    }
    

Complete the missing functions

  1. Create an error handler to easily display feedback to the user.

    private void showError(String message) {
        Log.d("FindRoute", message);
        Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
    }
    
  2. Call the setupOAuthManager() method from onCreate() after calling setupMap():

    setupMap();
    createGraphicsOverlay();
    // *** ADD ***
    setupOAuthManager();
    

Add a method for building routes

  1. Implement the code that finds a route from the two locations set by the user. Add a new method and call it findRoute():

    private void findRoute() {
        // Code from the next step goes here
    }
    
  2. Routing is handled with the RouteTask class. Create an instance of RouteTask with a routing service URL and load it:

        String routeServiceURI = getResources().getString(R.string.routing_url);
        final RouteTask solveRouteTask = new RouteTask(getApplicationContext(), routeServiceURI);
        solveRouteTask.loadAsync();
        solveRouteTask.addDoneLoadingListener(() -> {
            // Code from the next step goes here
        });
    
  3. Once the route task has been loaded, create routing parameters and add the mStart and mEnd points as stops:

            if (solveRouteTask.getLoadStatus() == LoadStatus.LOADED) {
                final ListenableFuture<RouteParameters> routeParamsFuture = solveRouteTask.createDefaultParametersAsync();
                routeParamsFuture.addDoneListener(() -> {
                    try {
                        RouteParameters routeParameters = routeParamsFuture.get();
                        List<Stop> stops = new ArrayList<>();
                        stops.add(new Stop(mStart));
                        stops.add(new Stop(mEnd));
                        routeParameters.setStops(stops);
                        // Code from the next step goes here
                    } catch (InterruptedException | ExecutionException e) {
                        showError("Cannot create RouteTask parameters " + e.getMessage());
                    }
                });
            } else {
                showError("Unable to load RouteTask " + solveRouteTask.getLoadStatus().toString());
            }
    
  4. Solve the route and select the first returned result:

                        final ListenableFuture<RouteResult> routeResultFuture = solveRouteTask.solveRouteAsync(routeParameters);
                        routeResultFuture.addDoneListener(() -> {
                            try {
                                RouteResult routeResult = routeResultFuture.get();
                                Route firstRoute = routeResult.getRoutes().get(0);
                                // Code from the next step goes here
                            } catch (InterruptedException | ExecutionException e) {
                                showError("Solve RouteTask failed " + e.getMessage());
                            }
                        });
    

Add code to draw the route on the map

  1. Create a Polyline and SimpleLineSymbol and use them to define a new Graphic and add the graphic to the graphics overlay:

                                Polyline routePolyline = firstRoute.getRouteGeometry();
                                SimpleLineSymbol routeSymbol = new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, Color.BLUE, 4.0f);
                                Graphic routeGraphic = new Graphic(routePolyline, routeSymbol);
                                mGraphicsOverlay.getGraphics().add(routeGraphic);
    
  2. Uncomment the call to findRoute() in the mapClicked() method in step 2.

  3. Press Control-R to run the app in the simulator. Tap the map to set a route start point. Tap the map again to set a route end point. If you have not previously logged in, the app will prompt you to log in with your ArcGIS for Developers account. After successful authentication the map should update with a route polyline drawn between the two points.

Congratulations, you're done!

Your map should now display a route between two tapped points. Compare your solution with our completed solution project.

Challenge

There are several ways this app could be enhanced. Can you take it to the next level?

Explore route parameters

The RouteParameters class offers a lot of options. Explore some of these options and enhance your app. Can you show turn-by-turn directions?

Get additional information about the route

The solution route, called firstRoute in this example, has additional information accessible through a series of .get...() statements. Add the following lines of code to findRoute() after getting firstRoute:

long lengthInKm = Math.round(firstRoute.getTotalLength() / 1000);
long timeInMinutes = Math.round(firstRoute.getTravelTime());

Toast.makeText(getApplicationContext(),
    "Total length (km): " + lengthInKm + " Travel time (min): " + timeInMinutes, Toast.LENGTH_LONG)
    .show();

Search and directions

Combine what you learned in Search for an address to allow your user to search for the start and end locations.

Handle multiple results

In this tutorial you handled the first result but ignored any additional results. Can you create a UI to allow the user to review multiple results?