Get driving directions

This ArcGIS Runtime SDK for Java tutorial describes how to build a Java app to find a route between two or more stops (known as routing) and show step-by-step directions for this route. You can customize the route by specifying preferences, such as whether to find the shortest or the fastest route, time windows for visiting each location, barriers representing road closures, and so on.

The API provides a RouteTask class to perform routing operations. The online route task relies on ArcGIS Server Network Analyst services. These services can be hosted in ArcGIS Online, on the Esri cloud platform, or on your on-premises servers. ArcGIS Online provides worldwide Network Analyst services you can use with a subscription to ArcGIS Online, or you can create your own services using ArcGIS for Server. You can also perform offline routing by providing your own route data and store it locally on the device.

Create the project in Eclipse

The following are concise steps on how to use the SDK's Eclipse plug-in to create a Java map app. See the Create a simple map application tutorial for detailed instructions on setting up an ArcGIS Runtime SDK for Java project using the Eclipse plug-in.

  1. In Eclipse, create a map application using the plug-in's template project: File > New > Project > ArcGIS Runtime for Java > ArcGIS Runtime Java Map Application.
  2. Using the New Project wizard, name the project and starting class DrivingDirections, and create it in a package of your choice, for example com.arcgis.java.tutorial.routing.

The DrivingDirections.java file now contains the base of the project you'll use in this tutorial.

Set up the map

You'll now create the ArcGIS Runtime SDK for Java components you'll use in the app, including layers, graphics, and symbols, and you'll prepare the map by setting an appropriate window size and extent, as well as adding layers to it.

  1. Add the SDK toolkit. In Eclipse, right-click the DrivingDirections project and select ArcGIS Tools > Add toolkit jar.
  2. Add the following field declarations at the top of the DrivingDirections class:
    // graphics layer for stops and whole route
    private GraphicsLayer graphicsLayer;
    // graphics layer for route segment graphics
    private GraphicsLayer routeSegmentsLayer;
    // Symbol for routes
    private SimpleLineSymbol routeSymbol = new SimpleLineSymbol(Color.BLUE, 5);
    // Overlay to draw your route stops
    private DrawingOverlay myDrawingOverlay;
    // object to perform routing
    private RouteTask task;
    // stop graphics
    private NAFeaturesAsFeature stops = new NAFeaturesAsFeature();

    For each field, hover over the class name (for example, GraphicsLayer) if it's underlined in red, and import the class from the context menu that appears. The build errors are fixed, and the red underline no longer displays. Alternatively, press Ctrl-Shift-O to import all of the unimported classes at once. When importing NAFeaturesAsFeature, ensure that you choose com.esri.core.tasks.na.NAFeaturesAsFeature, and for DrawingOverlay, choose com.esri.toolkit.overlays.DrawingOverlay.

    Note:

    Use Ctrl-Shift-O each time you use unimported classes.

    Adding these fields allows you to instantiate those objects later in your app. You'll create two graphics layers: one to show the route and stops, the other to show the route segment graphics for the turn-by-turn directions. You'll use a SimpleLineSymbol to symbolize the route graphics on the map, a DrawingOverlay to allow the user to draw the route stops on the map, and a RouteTask to perform the routing. Using fields instead of local variables in methods allows you to reuse the same objects in various parts of the app.

  3. Set the initial map extent to your area of interest, San Diego, create the graphics layers, and add the graphics layers to the map. The code from the template project creates the map with a basemap using MapOptions. Below the map = new JMap(mapOptions); line, add the following code:
    // set map extent to San Diego
    map.setExtent(new Envelope(-13054452, 3847753, -13017762, 3866957.78));
    
    // Graphics layers initialization
    graphicsLayer = new GraphicsLayer();
    routeSegmentsLayer = new GraphicsLayer();
    
    // add segments route layer first
    map.getLayers().add(routeSegmentsLayer);
    // add stops/route layer next, so that stops display above segments
    map.getLayers().add(graphicsLayer);
  4. Create a drawing overlay and add it to the map. The drawing overlay is a toolkit component that facilitates the process of having the user draw graphics on the map (in this case, the stop graphics). You'll implement the overlay's logic later in this tutorial. For now, add the following code below the previous step's code:
    // create drawing overlay and add to map
    myDrawingOverlay = new DrawingOverlay();
    map.addMapOverlay(myDrawingOverlay);

    You now have a launchable app showing a map centered on San Diego that fills the content pane of the window.

  5. In Eclipse, right-click the DrivingDirections.java file in the Package Explorer window and select Run As > Java Application to confirm the app works so far and displays a map of San Diego.
Screen shot of map centered on San Diego

Set up the UI

The steps in this section guide you through creating a user interface (UI) that includes components the user can use to interact with the app. The UI will have a toolbar with buttons to add stops to the map, find a route between these stops, and start a new route. There will also be a panel that shows directions for each step in the found route. The panel and map will be in a layered content pane, so that the panel can display on top of the map, and the toolbar will occupy the top portion of the application, adapting to the width of the application window.

  1. To add text in the UI, set up each string as a constant in the class. This allows you to manage all UI text in a single location, which makes it easier to find and update. You can also set up any service URLs as string constants to make them easy to manage as well. Add the following string constants to your app:
    // routing service
    private static final String ROUTE_URL = "http://sampleserver6.arcgisonline.com/arcgis/rest/services/NetworkAnalysis/SanDiego/NAServer/Route";
    // solve button string 
    private static final String SOLVE_BUTTON = " Solve route "; 
    // stop button string
    private static final String STOP_BUTTON = " Add a stop ";
    // turn-by-turn button string 
    private static final String TURN_BUTTON = " Turn by turn "; 
    // reset button string
    private static final String RESET_BUTTON = " Reset ";
  2. Add fields for the following Java Swing components you'll use in the app:
    // text area to display directions
    private JTextArea directionText;
    // turn by turn button
    private JButton stepsButton;
  3. Import the unimported classes by pressing Ctrl-Shift-O.
  4. Add the following fields below the code from step 2:
    private int numStops = 0;
    private int stepRoute = 0;
    ArrayList<Integer> stepIDs = new ArrayList<Integer>();

    The numStops variable stores the current number of stops added by the user, the stepRoute variable stores the current driving direction step, and the stepIDs ArrayList stores the graphic IDs of the route segments so that you can highlight them one by one on the map.

    Note:

    When you add a graphic to a graphics layer, the return value is a unique ID for the graphic in that layer. You can store this ID to later make updates to that particular graphic, such as selecting the graphic, updating its symbol, or updating its draw order.

  5. Create a content pane with a BorderLayout, and add it to the app window so that its contents fill the app window. To achieve this, add the following code at the bottom of the DrivingDirections constructor:
    JLayeredPane contentPane = new JLayeredPane();
    contentPane.setLayout(new BorderLayout());
    contentPane.setVisible(true);
    window.add(contentPane);
  6. Add the panel, toolbar, and map to the app content pane by adding the following code directly below the previous step's code:
    contentPane.add(createPanel());
    contentPane.add(map);
    contentPane.add(createToolBar(myDrawingOverlay), BorderLayout.NORTH);
  7. The createPanel() and createToolbar() method calls in the previous code appear with a red underline in Eclipse, because you have not yet created these methods. Hover over each method call in turn and select Create method createPanel() and Create method createToolBar(DrawingOverlay) to have Eclipse generate the method stub for you.
    private Component createPanel() {
      // TODO Auto-generated method stub
      return null;
    }
        
    private Component createToolBar(DrawingOverlay drawingOverlay) {
      // TODO Auto-generated method stub
      return null;
    }

    Ensure the parameter name in your createToolBar method is "drawingOverlay", as shown above. This is to avoid build errors in subsequent steps.

  8. Create the user panel and set the UI components to display the route content in the createPanel() method.
    // driving directions panel
    JComponent panel = new JPanel();
    panel.setLocation(10, 50);
    panel.setBackground(new Color(0, 0, 0, 100));
    panel.setBorder(new LineBorder(Color.BLACK, 1, false));
    panel.setLayout(new BorderLayout());
    panel.setSize(200, 150);
    
    // panel title
    JLabel txtTitle = new JLabel("Driving Directions");
    txtTitle.setHorizontalAlignment(SwingConstants.CENTER);
    txtTitle.setFont(new Font(txtTitle.getFont().getName(), Font.BOLD, 14));
    txtTitle.setAlignmentX(Component.CENTER_ALIGNMENT);
    txtTitle.setForeground(Color.WHITE);
    
    // scrolling text area to display direction text
    directionText = new JTextArea();
    directionText.setLineWrap(true);
    directionText.setWrapStyleWord(true);
    directionText.setEditable(false);
    directionText.setBorder(BorderFactory.createEmptyBorder(15,15,15,15));
    JScrollPane scrollPane = new JScrollPane(directionText);
    
    // button - when clicked, show turn by turn directions 
    stepsButton = new JButton(TURN_BUTTON);
    
    // group the above UI items into the JPanel
    panel.add(txtTitle, BorderLayout.NORTH);
    panel.add(scrollPane, BorderLayout.CENTER);
    panel.add(stepsButton, BorderLayout.SOUTH);

    Ensure that the method returns panel and not null.

  9. Create the toolbar with buttons for adding stops, solving the route, and resetting to start a new route. Fill in the createToolBar() method stub you created earlier with the following code:
    JToolBar toolBar = new JToolBar();
    toolBar.setLayout(new FlowLayout(FlowLayout.CENTER));
    
    // add stops button
    final JButton stopButton = new JButton(STOP_BUTTON);
    toolBar.add(stopButton);
    
    // solve route button
    final JButton solveRouteButton = new JButton(SOLVE_BUTTON);
    toolBar.add(solveRouteButton);
    
    // reset button
    JButton resetButton = new JButton(RESET_BUTTON);
    toolBar.add(resetButton);

    Be sure to change the return value of the method from null to the toolBar object.

    You now have a launchable app that shows a toolbar with three buttons at the top of the window, a map centered on San Diego, and a panel displayed in the top left corner of the map.

  10. In Eclipse, right-click the DrivingDirections.java file in the Package Explorer window and select Run As > Java Application to confirm the result of step 9.
Screen shot of map with toolbar and panel

Get locations from the user

In your app, you'll allow the user to specify the locations on the route by clicking the map to add stop locations. An alternative to obtaining locations from the user is to obtain addresses or place names from the user, then perform geocoding to obtain point coordinates for the input address text. In the interest of simplicity in this tutorial, you'll obtain locations from mouse clicks; the user will click the Add stops button on the toolbar, which will activate a drawing overlay to add point graphics. For subsequent clicks on the map, stop graphics will be added at the clicked points.

  1. In the createToolBar() method from above, add the following code below the line where you created the stopButton:
    stopButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        // Add a new stop graphic in the map overlay
        HashMap<String, Object> attributes = new HashMap<String, Object>();
        attributes.put("type", "Stop");
        drawingOverlay.setUp(
            DrawingMode.POINT,
            new SimpleMarkerSymbol(Color.BLUE, 25, Style.CIRCLE),
            attributes);
      }
    });

    You've added an ActionListener to the stop button, so that when the button is clicked, the drawing overlay is set up to draw point graphics (symbolized as blue circles) on the map. You added attributes to the added graphics to indicate that they're stop graphics. This is optional, because in this app, there are no other graphics added via mouse clicks.

    There is now a red underline under drawingOverlay. Hover over this element, and make this parameter "final". The build error will no longer display once you save the file.

    Dive-in:

    A final parameter cannot be re-assigned to a different object (or value). This allows the parameter to be accessible by an inner class, in this case the ActionListener class you have just defined. See final (Java) for more information on the final keyword.

  2. Add a listener to the drawing overlay so that when the user draws a graphic using the drawing overlay, you can retrieve it and add it to the graphics layer. In the DrivingDirections constructor, below the line where you created the drawing overlay, type myDrawingOverlay.addDrawingCompleteListener(new), type Ctrl-Space, and then type Enter to get the following method stub added for you:
    myDrawingOverlay.addDrawingCompleteListener(new DrawingCompleteListener() {
    
      @Override
      public void drawingCompleted(DrawingCompleteEvent event) {
        // TODO Auto-generated method stub      
      }
    });
  3. In the drawingCompleted method implementation, take the graphic drawn by the user in the drawing overlay (the blue circle, pursuant to how you set up the overlay), and add it to your graphics layer. Add a text symbol graphic over the circle graphic so that the user can see the order in which they added the stops. Replace the TODO line in the method stub above with the following:
    // get the user-drawn stop graphic from the overlay
    Graphic graphic = (Graphic) myDrawingOverlay.getAndClearFeature();
    // add it to the graphicsLayer for display
    graphicsLayer.addGraphic(graphic);
    // add to stops list for route task
    stops.addFeature(graphic);
    // add a text graphic showing the number of the current stop
    numStops++;
    Graphic textGraphic = new Graphic(
      graphic.getGeometry(), new TextSymbol(12, String.valueOf(numStops), Color.WHITE), 1);
    graphicsLayer.addGraphic(textGraphic);
  4. After you create the resetButton on the toolbar, add the following ActionListener so that the user can start fresh with a new route:
    resetButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        // enable the toolbar buttons and overlay  
        stopButton.setEnabled(true);
        solveRouteButton.setEnabled(true);
        myDrawingOverlay.setEnabled(true);
        // disable the turn by turn button
        stepsButton.setEnabled(false);
        // reset graphic layers, stop features and global variables
        directionText.setText("");
        graphicsLayer.removeAll();
        routeSegmentsLayer.removeAll();
        stops.clearFeatures();
        stepIDs.clear();
        numStops = 0;
        stepRoute = 0;
      }
    });

Calculate the route

To calculate a route, you first need to instantiate the route task. To do this, you need the URL of a routing service, and optionally, the user credentials to access the service. Since the service you'll be using in this tutorial is public, you don't need credentials to access it.

  1. Instantiate the route task once, at the bottom of the DrivingDirections constructor.
    try {
      task = RouteTask.createOnlineRouteTask(ROUTE_URL, null);
    } catch (Exception e) {
      e.printStackTrace();
      JOptionPane.showMessageDialog(map.getParent(), 
        "An error has occurred. " + e.getLocalizedMessage());
    }

    The route task creation can throw an exception, for example, if the credentials are invalid. Surround it with the required try-catch block, and display a message dialog box to the user if an error occurs.

  2. The optimal route between the added stops will be solved when the user clicks the Solve route button. Add an action listener to the solveRouteButton as follows:
    solveRouteButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        // disable the toolbar buttons and overlay 
        stopButton.setEnabled(false);
        solveRouteButton.setEnabled(false);
        myDrawingOverlay.setEnabled(false);
        // enable turn by turn button
        stepsButton.setEnabled(true);
        doRouting();
      }
    });
  3. You have not yet created the doRouting() method, so it appears with a red underline. Hover over it as before to create a method stub in the DrivingDirections class.
    protected void doRouting() {
      // TODO Auto-generated method stub
        
    }
  4. Implement the doRouting() method. Create empty result and parameters objects, then call the solve method on the RouteTask instance, passing in your parameters and getting the results back from the method call.
    // initialise route results and parameters
    RouteResult result = null;
    RouteParameters parameters = null;
    try {
      parameters = task.retrieveDefaultRouteTaskParameters();
      parameters.setOutSpatialReference(map.getSpatialReference());
      stops.setSpatialReference(map.getSpatialReference());
      parameters.setStops(stops);
      // set parameter to return turn by turn directions
      parameters.setReturnDirections(true);
      result = task.solve(parameters);
    } catch (Exception e) {
      e.printStackTrace();
      JOptionPane.showMessageDialog(map.getParent(), 
        "An error has occurred. " + e.getLocalizedMessage());
    }

Display route result

You called the solve method on the RouteTask instance and obtained a RouteResult instance back from the call. Now you'll add a method to display the result to the user.

  1. After the call to task.solve, add a method call to showResult, passing in the result instance.
    // ...
    result = task.solve(parameters);
    showResult(result);
  2. Hover over the method call, and generate a method stub for showResult.
  3. In the showResult method, check that you have a result, get the top route, and add all the route segments with their relevant information to the segments graphics layer, storing the graphic ID of each added segment. Then show the graphic for the whole route in your top-most graphics layer. When importing RouteTask, ensure it comes from the com.esri.core.tasks.na.RouteTask package.
    private void showResult(RouteResult result) {
      if (result != null) {
        // get the routing directions from the top route
        Route topRoute = result.getRoutes().get(0);
        topRoute.getRoutingDirections();
    
        // add route segments to the route layer
        for (RouteDirection rd : topRoute.getRoutingDirections()) {
          HashMap<String, Object> attribs = new HashMap<String, Object>();
          attribs.put("text", rd.getText());
          attribs.put("time", Double.valueOf(rd.getMinutes()));
          attribs.put("length", Double.valueOf(rd.getLength()));
          Graphic a = new Graphic(rd.getGeometry(), routeSymbol, attribs);
          int graphicID = routeSegmentsLayer.addGraphic(a);
          stepIDs.add(Integer.valueOf(graphicID));
        }
    
        // add the whole-route graphic
        Graphic routeGraphic = new Graphic(topRoute.getRouteGraphic().getGeometry(),
            new SimpleLineSymbol(Color.BLUE, 2.0f), 0);
        graphicsLayer.addGraphic(routeGraphic);
      }
      else {
        JOptionPane.showMessageDialog(map.getParent(), "No route found!");
      }
    }
  4. Get the full route summary, and display it to the user in the text area you created earlier. Zoom the map so that its new extent allows you to see the whole route with some extra space around it. Add the following code after graphicsLayer.addGraphic(routeGraphic) from the above code:
    // Get the full route summary and show in text area
    String routeSummary = String.format(
      "%s%nTotal time: %.1f minutes %nLength: %.1f miles",
      topRoute.getRouteName(), Double.valueOf(topRoute.getTotalMinutes()),
      Double.valueOf(topRoute.getTotalMiles()));
    directionText.setText(routeSummary);
    
    // Zoom to the extent of the whole route plus a 500m buffer
    Polygon bufferedExtent = GeometryEngine.buffer(
      topRoute.getEnvelope(), map.getSpatialReference(), 500, null);
    map.setExtent(bufferedExtent);
  5. Run the application. Add stops by clicking the map. Click the Solve route button, notice the top route result displayed, and then clear the graphics by clicking the Reset button.
Screen shot of the app with stops and the top route displaying

Display turn-by-turn directions

In this section, you'll learn how to show turn-by-turn directions in the text area, and implement the Turn by turn button so that when clicked, the next direction string is displayed with its corresponding route segment highlighted on the map.

  1. Set a highlight color of the graphics layer containing your route segments so that when you select a graphic in this layer, it will show with a glow (highlight). In the DrivingDirections constructor, after you create the routeSegmentsLayer, set a suitable selection color such as red.
    routeSegmentsLayer.setSelectionColor(Color.RED);
  2. Add an action listener method stub for your Turn by turn button, as you did previously for the other buttons. Add the following after you create the stepsButton object in the createPanel method:
    stepsButton.addActionListener(new ActionListener() {
          
      @Override
      public void actionPerformed(ActionEvent e) {
        // TODO Auto-generated method stub
            
      }
    });
  3. Each click of the Turn by turn button should show the next direction string and highlight the corresponding route segment. In the button action listener, call a method named doTurnByTurn to perform these tasks. Add the method call to the listener and create the method stub.
    stepsButton.addActionListener(new ActionListener() {
          
      @Override
      public void actionPerformed(ActionEvent e) {
        doTurnByTurn();
      }
    });
  4. Add the following code in your doTurnByTurn method stub. Get the next graphic ID from the segment graphics layer, highlight the route segment on the map, and display the direction text in your text area. If there are no directions remaining, indicate that it is the end of the route, and disable the Turn by turn directions button.
    // check there is a next step in the route
    if (stepRoute < routeSegmentsLayer.getNumberOfGraphics()) {
      Graphic selected = routeSegmentsLayer.getGraphic(stepIDs.get(stepRoute).intValue());   
      // Highlight route segment on the map
      routeSegmentsLayer.select(stepIDs.get(stepRoute).intValue());
      String direction = ((String) selected.getAttributeValue("text"));
      Double time = (Double) selected.getAttributeValue("time");
      Double length = (Double) selected.getAttributeValue("length");
      // Update the label with this direction's information
      String label = String.format("%s%nTime: %.1f minutes, Length: %.1f miles",
        direction, time, length);
      directionText.setText(label);
      stepRoute++;
    } else {
      directionText.setText("End of route");
      stepsButton.setEnabled(false);
    }

That's it! You've built an app using ArcGIS Runtime SDK for Java to perform routing and display turn-by-turn directions to the user.

Now, do a simple test run of your app:

  1. Right-click the DrivingDirections.java class and select Run As > Java Application.
  2. Click the Add a stop button, and add a few stops to the map, using mouse clicks, in the San Diego area where the map is centered.
  3. Click the Solve route button; a route should appear, linking all your stops together, and the map should zoom to the extent of the route. The text area should be showing a route summary.
  4. Click the Turn by turn button; the direction text should change, and the corresponding route segment should be showing with a red highlight on the map, along with the previously shown segments.
  5. Click the Reset button, which should clear all the graphics and the text area, and try another route to make sure the app can be run multiple times.

You can now extend your app, for example, by adding a button for the user to add barriers (representing road closures), by obtaining locations via a text field where the user can enter place names or addresses, or by allowing more parameters to be set for the route task, such as not preserving the order of stops.