Overview

You will learn: how to find the optimal route and directions offline for multiple stops with the ArcGIS Online Network Analytics Route service and StreetMap Premium.

StreetMap Premium provides high-quality data optimized for cartographic map display, geocoding, and routing. StreetMap Premium data is delivered via mobile map packages (.mmpk). These can be used for routing and geocoding while offline. You can use a RouteTask and the SolveRouteAsync method to find routes. RouteTask accepts "stop" locations and returns a route with directions. Once you have the results you can add the route to a map, display the turn-by-turn directions, or otherwise integrate them into your application.

In this lab you will learn how to use a transportation network from StreetMap Premium to calculate an optimal route between two stops. Stop locations will be selected with clicks on the map.

Before you begin

Reuse the starter project

If you have not done the starter project lab, be sure to review it first so you are familiar with running the app.

In a new or empty folder, make a copy of the Create a starter app solution.

Complete or review labs Add layers to a map (offline), Create graphics, Display point, line, and polygon graphics, and [Get a route and directions as you will repeat some parts of those labs here.

Steps

Update the project

  1. Open the settings.gradle file in an editor of your choice and change the rootProject.name value to get-a-route-and-directions-offine.

Load the network from StreetMap Premium

  1. Your app will need to either include the StreetMap Premium .mmpk files, or be able to download them on demand. For this lab, the data will be provided, California package. Add this to the root of the project.

Imports and variable declarations

  1. Open the App.java file in src/main/java/com/arcgis/developers/labs. Then add the following with the existing imports.

    import java.util.Arrays;
    import java.util.concurrent.ExecutionException;
    
    import javafx.geometry.Point2D;
    import javafx.scene.control.Alert;
    import javafx.scene.input.MouseButton;
    
    import com.esri.arcgisruntime.concurrent.ListenableFuture;
    import com.esri.arcgisruntime.data.TransportationNetworkDataset;
    import com.esri.arcgisruntime.geometry.Point;
    import com.esri.arcgisruntime.geometry.Polyline;
    import com.esri.arcgisruntime.loadable.LoadStatus;
    import com.esri.arcgisruntime.mapping.MobileMapPackage;
    import com.esri.arcgisruntime.mapping.Viewpoint;
    import com.esri.arcgisruntime.mapping.view.Graphic;
    import com.esri.arcgisruntime.mapping.view.GraphicsOverlay;
    import com.esri.arcgisruntime.symbology.SimpleLineSymbol;
    import com.esri.arcgisruntime.symbology.SimpleMarkerSymbol;
    import com.esri.arcgisruntime.tasks.networkanalysis.Route;
    import com.esri.arcgisruntime.tasks.networkanalysis.RouteParameters;
    import com.esri.arcgisruntime.tasks.networkanalysis.RouteResult;
    import com.esri.arcgisruntime.tasks.networkanalysis.RouteTask;
    import com.esri.arcgisruntime.tasks.networkanalysis.Stop;
    
  2. Add new private fields that will be used later.

    /* ** ADD ** */
    private Point startPoint;
    private Point endPoint;
    private final int hexRed = 0xFFFF0000;
    private final int hexBlue = 0xFF0000FF;
    
    private GraphicsOverlay graphicsOverlay;
    private RouteTask solveRouteTask;
    private RouteParameters routeParameters;
    private TransportationNetworkDataset transportationNetwork;
    /* ** ADD ** */
    private MapView mapView;
    

Add graphics overlay to a map view

  1. Add a new private method setupGraphicsOverlay that creates a graphics overlay and attaches it to the map view to render the graphics to the map. Each graphic must have a symbol assigned in order for it to be visible on the map.

    private void setupGraphicsOverlay() {
      if (mapView != null) {
        graphicsOverlay = new GraphicsOverlay();
        mapView.getGraphicsOverlays().add(graphicsOverlay);
      }
    }
    
  2. Make a call to the method just created in the start method.

    setupMap();
    
    /* ** ADD ** */
    setupGraphicsOverlay();
    

Load and display the map package

  1. Add a new private method setupMobileMap that loads a mmpk file into a map object and then updates the map view to display it. The map package is loaded asynchronously so it does not slow the UI. If the map package fails to load for any reason, we fallback to the online map we had before.

      private void setupMobileMap() {
        if (mapView != null) {
          String mmpkFile = "./California.mmpk";
          final MobileMapPackage mapPackage = new MobileMapPackage(mmpkFile);
          mapPackage.addDoneLoadingListener(() -> {
            if (mapPackage.getLoadStatus() == LoadStatus.LOADED && mapPackage.getMaps().size() > 0) {
    
            } else {
              setupMap();
              new Alert(Alert.AlertType.ERROR, "MMPK failed to load: " + mapPackage.getLoadError().getMessage())
               .show();
            }
          });
          mapPackage.loadAsync();
        }
      }
    
  2. Set map from map package to the map view and get TransportationNetworkDataset from that map.

    if (mapPackage.getLoadStatus() == LoadStatus.LOADED && mapPackage.getMaps().size() > 0) {
      /* ** ADD ** */
      double latitude = 34.05293;
      double longitude = -118.24368;
      double scale = 220000;
      ArcGISMap map = mapPackage.getMaps().get(0);
      transportationNetwork = map.getTransportationNetworks().get(0);
      mapView.setMap(map);
      map.setInitialViewpoint(new Viewpoint(latitude, longitude, scale));
      //setupRouteTask();
    
  3. Look for these lines of the code in the start method

    mapView.setMap(map);
    setupMap();
    

    and replace setupMap() with setupMobileMap():

    mapView.setMap(map);
    setupMobileMap();
    

Create Route Task

  1. Initialize and load the route task using the transportation network. The route task will calculate a route between two points using the detailed information transportation network.

    private void setupRouteTask() {
      solveRouteTask = new RouteTask(transportationNetwork);
      solveRouteTask.loadAsync();
    }
    
  2. Once the route task has loaded create default route parameters.

    solveRouteTask.loadAsync();
    
    /* ** ADD ** */
    solveRouteTask.addDoneLoadingListener(() -> {
      if (solveRouteTask.getLoadStatus() == LoadStatus.LOADED) {
        final ListenableFuture<RouteParameters> routeParamsFuture = solveRouteTask.createDefaultParametersAsync();
        routeParamsFuture.addDoneListener(() -> {
    
          try {
            routeParameters = routeParamsFuture.get();
            //createRouteAndDisplay();
          } catch (InterruptedException | ExecutionException e) {
            new Alert(Alert.AlertType.ERROR, "Cannot create RouteTask parameters " + e.getMessage()).show();
          }
        });
      } else {
        new Alert(Alert.AlertType.ERROR, "Unable to load RouteTask " + solveRouteTask.getLoadStatus().toString()).show();
      }
    });
    
  3. Uncomment setupRouteTask from setupMobileMap method.

    map.setInitialViewpoint(new Viewpoint(latitude, longitude, scale));
    setupRouteTask();
    

Methods for displaying route start and end locations

  1. Add a private method setMapMarker that creates a graphic at a specified location using a symbol and display it to the map.

     private void setMapMarker(Point location, SimpleMarkerSymbol.Style style, int markerColor, int outlineColor) {
       final float markerSize = 8;
       final float markerOutlineThickness = 2;
       SimpleMarkerSymbol pointSymbol = new SimpleMarkerSymbol(style, markerColor, markerSize);
       pointSymbol.setOutline(new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, outlineColor, markerOutlineThickness));
       Graphic pointGraphic = new Graphic(location, pointSymbol);
       graphicsOverlay.getGraphics().add(pointGraphic);
     }
    
  2. Add a new private method setStartMarker that visually displays and stores the starting location.

     private void setStartMarker(Point location) {
       graphicsOverlay.getGraphics().clear();
       setMapMarker(location, SimpleMarkerSymbol.Style.DIAMOND, hexRed, hexBlue);
       startPoint = location;
       endPoint = null;
     }
    
  3. Add a new private method setEndMarker that visually displays and stores the ending location.

     private void setEndMarker(Point location) {
       setMapMarker(location, SimpleMarkerSymbol.Style.SQUARE, hexBlue, hexRed);
       endPoint = location;
       // solveForRoute();
     }
    

Add listener for mouse interactions

  1. Add a private method createRouteAndDisplay that adds a listener to the map view for interpreting user input when the map is clicked. Then call the graphic display methods you coded in the prior step.

     private void createRouteAndDisplay() {
       mapView.setOnMouseClicked(e -> {
         if (e.getButton() == MouseButton.PRIMARY && e.isStillSincePress()) {
           Point2D point = new Point2D(e.getX(), e.getY());
           Point mapPoint = mapView.screenToLocation(point);
    
           if (startPoint == null) {
             setStartMarker(mapPoint);
           } else if (endPoint == null) {
             setEndMarker(mapPoint);
           } else {
             setStartMarker(mapPoint);
           }
         }
       });
     }
    
  2. Uncomment createRouteAndDisplay from setupRouteTask method.

     routeParameters = routeParamsFuture.get();
     createRouteAndDisplay();
    

Create and Display a Route

  1. Add a private method solveForRoute that solves for a route between two locations and displays that route on the map.

     private void solveForRoute() {
    
       if (startPoint != null && endPoint != null) {
         routeParameters.setStops(Arrays.asList(new Stop(startPoint), new Stop(endPoint)));
    
         final ListenableFuture<RouteResult> routeResultFuture = solveRouteTask.solveRouteAsync(routeParameters);
       }
     }
    
  2. Wait for task to be complete before accessing the results.

     final ListenableFuture<RouteResult> routeResultFuture = solveRouteTask.solveRouteAsync(routeParameters);
     /* ** ADD ** */
     routeResultFuture.addDoneListener(() -> {
       try {
         RouteResult routeResult = routeResultFuture.get();
       } catch (InterruptedException | ExecutionException e) {
         new Alert(Alert.AlertType.ERROR, "Solve RouteTask failed " + e.getMessage() + e.getMessage()).show();
       }
     });
    
  3. Check that a route was returned and selected the first one. Display this route to the map using the graphics overlay.

      RouteResult routeResult = routeResultFuture.get();
      /* ** ADD ** */
      if(routeResult.getRoutes().size() > 0) {
        Route firstRoute = routeResult.getRoutes().get(0);
    
        Polyline routePolyline = firstRoute.getRouteGeometry();
        SimpleLineSymbol routeSymbol = new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, hexBlue, 4.0f);
        Graphic routeGraphic = new Graphic(routePolyline, routeSymbol);
        graphicsOverlay.getGraphics().add(routeGraphic);
       } else {
        new Alert(Alert.AlertType.WARNING, "No routes have been found.").show();
      }
    
  4. Uncomment the call to solveForRoute() in the setEndMarker method.

     endPoint = location;
     solveForRoute();
    

Congratulations, you're done!

Your map displays a route between two clicked locations. Compare with the completed solution project.

Challenge

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 properties. Enhance your app by displaying additional information about the route solution, such as travel time and route length.

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

 new Alert(Alert.AlertType.INFORMATION, "Total length (km): " + lengthInKm + " Travel time (min): " + timeInMinutes).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 lab you handled the first result but ignored any additional results. Can you create a UI to allow the user to review multiple results?