Overview

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

The ArcGIS Transportation 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 pass in the "stop" locations, and the service 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 integrate them further into your application. To learn more about the capabilities of the directions and routing service, please visit the ArcGIS REST API Documentation.

In this lab you will learn how to use a routing service to calculate an optimal route between two stops. Stop locations will be selected with taps on the map.

Before you begin

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

Configure OAuth2 for your app

  1. Using the ArcGIS Transportation Routing and Network Analytics Services requires an authenticated ArcGIS Online user and a valid token. Register your app to generate a client id and set a redirect URI in order to use OAuth 2:

    1. Sign into ArcGIS for Developers.
    2. Select Dashboard + > New Application to create a new application.
    3. Fill in new application details then select Register New Application. Take note of the Client ID that is automatically assigned to your app.
    4. On the Authentication tab use the Redirect URIs section to add my-devlab-app://auth.

Steps

Create a new ArcGIS Runtime App Visual Studio Project

  1. Start Visual Studio.

  2. Choose File > New > Project and select the ArcGIS Runtime Application (WPF) template for Visual C#.

Declare variables and using statements

  1. Add the following using statements at the top of your MapViewModel.cs code module:

    using Esri.ArcGISRuntime.Tasks.NetworkAnalysis;
    using Esri.ArcGISRuntime.UI.Controls;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;
    using System.Windows.Navigation;
    
  2. Your app will track the start and end points (stops) for the route and display the route geometry using a GraphicsOverlay. Add variables to store required information and to support OAuth2 authentication in MapViewModel.cs:

        private Map _map;
        private MapView _mapView;
        private GraphicsOverlay _graphicsOverlay;
        private MapPoint _startPoint;
        private MapPoint _endPoint;
        private String ServerUrl = "https://www.arcgis.com/sharing/rest";
        private String RouteServiceURI = "https://route.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World";
        private String ClientId = "YOUR-APP-CLIENT-ID";
        private String RedirectURI = "my-devlab-app://auth";
    

Add authentication

  1. Using the ArcGIS Transportation Routing and Network Analytics Services requires an authenticated user and a valid token. The .NET SDK handles this for you with AuthenticationManager. To complete OAuth authorization, a web page is presented to your user that prompts for credentials (username and password) directly from arcgis.com. When complete, your app gets the status of that event. In a desktop (WPF) app, an OAuthAuthorizeHandler component handles many of the OAuth details. Add the implementation of this class at the bottom of your MapViewModel.cs module (inside the application namespace).

    public class OAuthAuthorize : IOAuthAuthorizeHandler
    {
        private Window _window;
        private TaskCompletionSource<IDictionary<string, string>> _tcs;
        private string _callbackUrl;
        private string _authorizeUrl;
    
        public Task<IDictionary<string, string>> AuthorizeAsync(Uri serviceUri, Uri authorizeUri, Uri callbackUri)
        {
            if (_tcs != null || _window != null)
            {
                throw new Exception();
            }
            _authorizeUrl = authorizeUri.AbsoluteUri;
            _callbackUrl = callbackUri.AbsoluteUri;
            _tcs = new TaskCompletionSource<IDictionary<string, string>>();
            var dispatcher = Application.Current.Dispatcher;
            if (dispatcher == null || dispatcher.CheckAccess())
            {
                AuthorizeOnUIThread(_authorizeUrl);
            }
            else
            {
                dispatcher.BeginInvoke((Action)(() => AuthorizeOnUIThread(_authorizeUrl)));
            }
            return _tcs.Task;
        }
    
        private void AuthorizeOnUIThread(string authorizeUri)
        {
            var webBrowser = new WebBrowser();
            webBrowser.Navigating += WebBrowserOnNavigating;
            _window = new Window
            {
                Content = webBrowser,
                Height = 600,
                Width = 400,
                WindowStartupLocation = WindowStartupLocation.CenterOwner,
                Owner = Application.Current != null && Application.Current.MainWindow != null
                            ? Application.Current.MainWindow
                            : null
            };
            _window.Closed += OnWindowClosed;
            webBrowser.Navigate(authorizeUri);
            if (_window != null)
            {
                _window.ShowDialog();
            }
        }
    
        void OnWindowClosed(object sender, EventArgs e)
        {
            if (_window != null && _window.Owner != null)
            {
                _window.Owner.Focus();
            }
            if (_tcs != null && !_tcs.Task.IsCompleted)
            {
                // The user closed the window
                _tcs.SetException(new OperationCanceledException());
            }
            _tcs = null;
            _window = null;
        }
    
        void WebBrowserOnNavigating(object sender, NavigatingCancelEventArgs navigationEvent)
        {
            var webBrowser = sender as WebBrowser;
            Uri uri = navigationEvent.Uri;
            if (webBrowser == null || uri == null || _tcs == null || string.IsNullOrEmpty(uri.AbsoluteUri))
            {
                return;
            }
            if (uri.AbsoluteUri.StartsWith(_callbackUrl))
            {
                navigationEvent.Cancel = true;
                var tcs = _tcs;
                _tcs = null;
                if (_window != null)
                {
                    _window.Close();
                }
                tcs.SetResult(DecodeParameters(uri));
            }
        }
    
        private static IDictionary<string, string> DecodeParameters(Uri uri)
        {
            var answer = string.Empty;
            if (!string.IsNullOrEmpty(uri.Fragment))
            {
                answer = uri.Fragment.Substring(1);
            }
            else if (!string.IsNullOrEmpty(uri.Query))
            {
                answer = uri.Query.Substring(1);
            }
            var keyValueDictionary = new Dictionary<string, string>();
            var keysAndValues = answer.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var kvString in keysAndValues)
            {
                var pair = kvString.Split('=');
                string key = pair[0];
                string value = string.Empty;
                if (key.Length > 1)
                {
                    value = Uri.UnescapeDataString(pair[1]);
                }
                keyValueDictionary.Add(key, value);
            }
            return keyValueDictionary;
        }
    }
    
  2. Create a function to handle authentication challenges in your app. The following method is called by the AuthenticationManager whenever the app attempts to access secured content. Add this code as a method of your MapViewModel class.

        public async Task<Credential> CreateCredentialAsync(CredentialRequestInfo credentialRequestInfo)
        {
            Credential credential = null;
            try
            {
                credential = await AuthenticationManager.Current.GenerateCredentialAsync(credentialRequestInfo.ServiceUri);
            }
            catch (Exception ex)
            {
                throw (ex);
            }
            return credential;
        }
    
  3. Add the following function to your MapViewModel class to register ArcGIS Online server information with AuthenticationManager and define the challenge handler function. You'll also set the AuthenticationManager to use the OAuthAuthorize class you created previously to handle OAuth authorization requests.

        private void SetOAuthInfo()
        {
            var serverInfo = new ServerInfo
            {
                ServerUri = new Uri(ServerUrl),
                TokenAuthenticationType = TokenAuthenticationType.OAuthImplicit,
                OAuthClientInfo = new OAuthClientInfo
                {
                    ClientId = ClientId,
                    RedirectUri = new Uri(RedirectURI)
                }
            };
            AuthenticationManager.Current.RegisterServer(serverInfo);
            AuthenticationManager.Current.OAuthAuthorizeHandler = new OAuthAuthorize();
            AuthenticationManager.Current.ChallengeHandler = new ChallengeHandler(CreateCredentialAsync);
        }
    

Load the basemap

  1. Add the following function to the MapViewModel class to create the basemap and set the initial viewpoint:

        private void CreateNewMap()
        {
            Map = new Map(BasemapType.StreetsNightVector, 34.05293, -118.24368, 11);
        }
    
  2. Update the MapViewModel constructor to call the new functions:

        public MapViewModel()
        {
            CreateNewMap();
            SetOAuthInfo();
        }
    

Add helper functions

  1. Create a set function to set a reference to the map view and another function to create a graphics overlay only if it has not been created.

        public MapView MapView
        {
            set { _mapView = value; }
        }
    
        private void SetGraphicsOverlay()
        {
            if (_mapView != null && _graphicsOverlay == null)
            {
                _graphicsOverlay = new GraphicsOverlay();
                _mapView.GraphicsOverlays.Add(_graphicsOverlay);
            }
        }
    

Track user route request

  1. Add new methods to MapViewModel to create marker symbols representing the start and end locations of the route request from user mouse clicks (or screen taps).

        private void SetMapMarker(MapPoint location, SimpleMarkerSymbolStyle pointStyle, Color markerColor, Color markerOutlineColor)
        {
            float markerSize = 8.0f;
            float markerOutlineThickness = 2.0f;
            SimpleMarkerSymbol pointSymbol = new SimpleMarkerSymbol(pointStyle, markerColor, markerSize);
            pointSymbol.Outline = new SimpleLineSymbol(SimpleLineSymbolStyle.Solid, markerOutlineColor, markerOutlineThickness);
            Graphic pointGraphic = new Graphic(location, pointSymbol);
            _graphicsOverlay.Graphics.Add(pointGraphic);
        }
    
        private void SetStartMarker(MapPoint location)
        {
            SetGraphicsOverlay();
            _graphicsOverlay.Graphics.Clear();
            setMapMarker(location, SimpleMarkerSymbolStyle.Diamond, Color.FromRgb(226, 119, 40), Color.FromRgb(0, 226, 0));
            _startPoint = location;
            _endPoint = null;
        }
    
        private void SetEndMarker(MapPoint location)
        {
            setMapMarker(location, SimpleMarkerSymbolStyle.Square, Color.FromRgb(40, 119, 226), Color.FromRgb(226, 0, 0));
            _endPoint = location;
            // FindRoute();
        }
    
  2. Add a new method to MapViewModel to interpret user input when the map view is clicked or tapped and call the symbol display functions you coded in the prior step:

        public void MapClicked(MapPoint location)
        {
            if (_startPoint == null)
            {
                SetStartMarker(location);
            }
            else if (_endPoint == null)
            {
                SetEndMarker(location);
            }
            else
            {
                SetStartMarker(location);
            }
        }
    

Connect the map view

  1. You need a reference to the MapView in order to add a GraphicsOverlay to it. Open the source file MainWindow.xaml and update the MapView such that is has a name identifier to allow you to get a reference to it in code.

        <esri:MapView x:Name="EsriMapView" Map="{Binding Map, Source={StaticResource MapViewModel}}">
        </esri:MapView>
    
  2. In the code-behind source file MainWindow.xaml.cs, create a variable in the MainWindow class to hold a reference to the MapViewModel associated with the page:

    private MapViewModel _mapViewModel;
    
  3. Add code to the MainWindow constructor to set a reference to MapView on MapViewModel:

    public MainWindow()
    {
        InitializeComponent();
    
        // **Add this code**
        _mapViewModel = this.FindResource("MapViewModel") as MapViewModel;
        _mapViewModel.MapView = EsriMapView;
        // **
    }
    

Handle user input

  1. Add a handler to respond to taps/clicks on the MapView and connect it to the MapClicked method of the MapViewModel:

        private void EsriMapView_GeoViewTapped(object sender, Esri.ArcGISRuntime.UI.Controls.GeoViewInputEventArgs geoViewInputEvent)
        {
            _mapViewModel.MapClicked(geoViewInputEvent.Location);
        }
    
  2. Go back to the MainWindow constructor and wire up the event handler:

        EsriMapView.GeoViewTapped += EsriMapView_GeoViewTapped;
    

At this point you can run and test your app. The app loads a map with the vector streets basemap and initial viewpoint centered on Los Angeles. You can click the map once to show a diamond marker. Click a second time and a square marker appears at that location on the map. Asking for login and diplaying a route does not take place, you will make that work in the following set of steps.

Solve the route

  1. Implement code that finds a route between the two stops defined by the user. Return to the MapViewModel class defined in MapViewModel.cs. Add the following FindRoute method:

        private async void FindRoute()
        {
            try
            {
                // Code for the following steps goes here ...
            }
            catch (Exception exception)
            {
                throw (exception);
            }
        }
    
  2. Routing is handled with the RouteTask class. Await the creation of an instance of RouteTask with the routing service URL and get its default route parameters:

            RouteTask solveRouteTask = await RouteTask.CreateAsync(new Uri(RouteServiceURI));
            RouteParameters routeParameters = await solveRouteTask.CreateDefaultParametersAsync();
    
  3. Add the _startPoint and _endPoint points as stops to the RouteParameters:

            List<Stop> stops = new List<Stop> { new Stop(_startPoint), new Stop(_endPoint) };
            routeParameters.SetStops(stops);
    
  4. Solve the route and select the first returned result:

            RouteResult solveRouteResult = await solveRouteTask.SolveRouteAsync(routeParameters);
            Route firstRoute = solveRouteResult.Routes.FirstOrDefault();
    

Add code to draw the route on the map view

  1. Create a new Graphic to display the route geometry (Polyline) with a SimpleLineSymbol. Add the graphic to the graphics overlay:

            Polyline routePolyline = firstRoute.RouteGeometry;
            SimpleLineSymbol routeSymbol = new SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Colors.GreenYellow, 4.0f);
            Graphic routeGraphic = new Graphic(routePolyline, routeSymbol);
            _graphicsOverlay.Graphics.Add(routeGraphic);
    
  2. Uncomment the call to FindRoute() in the SetEndMarker method.

Congratulations, you're done!

Your map displays a route between two tapped locations. The first time a route is requested, the AuthenticationManager will request a login. Compare with the 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 properties. Enhance your app by displaying additional information about the route solution, such as travel time and route length.

int lengthInKm = (int)Math.Round(firstRoute.TotalLength / 1000);
int timeInMinutes = (int)Math.Round(firstRoute.TravelTime.TotalMinutes);
MessageBox.Show("Total length (km): " + lengthInKm + " Travel time (min): " + timeInMinutes, "Route Info");

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?