Skip To Content ArcGIS for Developers Sign In Dashboard

ArcGIS API for Python

Guide to Network Analysis (Part 2 - Find Routes)

Introduction

Now we have learned the concepts of Network Datasets and Network Analysis services, let's move on the second topic - how to find routes from one point to another, and/or among multiple points. This would solve questions like,

  • What is the quickest route from point A to point B?
  • To travel among points [A, B, C, D, ...], what's the shorted route?

Please refer to the road map below if you want to revisit the previous topic or jump to the next topic.

  • Network Dataset and Network Analysis services (Part 1)
  • Find Routes (You are here!)
  • Generate Service Area (Part 3)
  • Find Closest Facility (Part 4)
  • Generate Origin Destination Cost Matrix (Part 5)
  • Solve Location Allocation (Part 6)
  • Vehicle Routing Problem Service (Part 7)

Routes represent the quickest or shortest path along roads to visit stops or point locations. They can be basic point-to-point routes visited in the order you specify, or in the order that minimizes overall travel time or distance. A route is associated with a local network dataset or a network service hosted in ArcGIS Online or ArcGIS Enterprise [1]. Here, we will show how to use the online service to solve routing problems.

Problem statement

Centering at how to find routes among multiple points, we can assume the user story is like this:

Jim is planning his Summer trip along the west of the States, and he has decided to start from Miami, end at San Francisco, and also make stops in eight other cities: San Jose, Los Angeles, San Diego, Phoenix, El Paso, Houston, New Orleans, Orlando. What will be the best route for Jim to travel between these ten cities without making extra stops? And what will be the estimated travel distance and time? Lastly, can Jim be provided with directions and maps for routing?

Now that Jim's objectives are defined, we can go onto breaking down the problem:

  • Data: where to access the input dataset
  • Methods: what tools can be used to build the network model and perform routing analysis
  • Tables and maps: deliverables in which directions and routes are visualized.

Let's first access and/or explore the input dataset (in this case, the stops feature class).

Data

Access the Stops Feature Class

The find_routes tool requires a stops feature class as the input parameter, which should be of a FeatureSet type specifying two or more stops to route between. The upper limit is for the stops feature class to have 10,000 stops or up to 150 stops assigned to a single route.

In this example, we will extract the stops feature class from an existing Feature Service. First of all, we need to import all required modules, and establish a GIS connection object to the target organization.

In [1]:
from copy import deepcopy
import datetime as dt
from IPython.display import HTML
import json
import pandas as pd
from arcgis.gis import GIS
import arcgis.network as network
import arcgis.geocoding as geocoding
from arcgis.features import FeatureLayer, FeatureSet, FeatureCollection
import arcgis.features.use_proximity as use_proximity

If you have already set up a profile to connect to your ArcGIS Online organization, execute the cell below to load the profile and create the GIS class object. If not, use a traditional username/password log-in e.g. my_gis = GIS('https://www.arcgis.com', 'arcgis_python', 'P@ssword123', verify_cert=False, set_active=True)

In [2]:
my_gis = GIS(profile="your_online_profile")

The existing Feature Layer to be used is titled "USA Major Cities", and can be accessed through a search call.

In [3]:
sample_cities = my_gis.content.search('title:"USA Major Cities" type:Feature Service owner:esri*', 
                                      outside_org=True)[0]
sample_cities
Out[3]:
USA Major Cities
This layer presents the locations of cities within the United States with populations of approximately 10,000 or greater, all state capitals, and the national capital.Feature Layer Collection by esri_dm
Last Modified: August 22, 2019
0 comments, 1,918,089 views

Place the cities to be visited in a list, and provide a formatter that will later be used to format the list into a string.

In [4]:
stops_cities = ['San Francisco', 'San Jose', 'Los Angeles', 'San Diego',
                'Phoenix', 'El Paso', 
                'Houston', 'New Orleans', 'Orlando', 'Miami']
values = "'" + "', '".join(stops_cities) + "'"
In [5]:
stops_cities_fl = FeatureLayer(sample_cities.url + "/0")
type(stops_cities_fl)
Out[5]:
arcgis.features.layer.FeatureLayer

With the FeatureLayer class object defined, make query calls to get back the cities as FeatureSet.

In [6]:
stops_cities_fset = stops_cities_fl.query(where="ST in ('CA', 'NV', 'TX', 'AZ', 'LA', 'FL')  AND NAME IN ({0})".format(values), as_df=False)
stops_cities_fset
Out[6]:
<FeatureSet> 10 features
In [7]:
start_cities_fset = stops_cities_fl.query(where="ST='FL' AND NAME = 'Miami'", as_df=False)
start_cities_fset
Out[7]:
<FeatureSet> 1 features

Re-arrange the order of stops

Sometimes features in the returned resulting FeatureSet from FeatureLayer.query() are not in the same sequential order of the search string. For example, the starting city in stops_cities is "San Francisico", but in the returned FeatureSet stops_cities_fset, "Los Angeles" appears to be the start.

In [8]:
print(list(map(lambda x: x.attributes['NAME'], stops_cities_fset)))
['Los Angeles', 'San Diego', 'San Francisco', 'San Jose', 'Phoenix', 'New Orleans', 'Miami', 'Orlando', 'El Paso', 'Houston']

The method re_order_stop_cities can be used to re-order the stops in the FeatureSet to match the original starting/ending sequence.

In [9]:
""" Used to re-order the stops to the desired order
"""
def re_order_stop_cities(fset=stops_cities_fset, start_city = "Miami", end_city = "San Francisco"):
    
    stops_cities_flist = []
    last_city = None

    for ea in fset:
        if ea.attributes['NAME'] == start_city:
            stops_cities_flist.insert(0, ea)
        elif ea.attributes['NAME'] == end_city:
            last_city = ea
        else:
            stops_cities_flist.append(ea)
    stops_cities_flist.append(last_city)
 
    return FeatureSet(stops_cities_flist)
In [10]:
re_ordered_stops_cities_fset = re_order_stop_cities()
re_ordered_stops_cities_fset
Out[10]:
<FeatureSet> 10 features

Also we need to assign spatialReference to the newly sorted FeatureSet, or else, an error would be thrown at the solver as Invalid value for parameter Stops - Details : Invalid JSON for GPFeatureRecordSetLayer or GPRecordSet datatype Failed.

In [11]:
re_ordered_stops_cities_fset.spatial_reference = stops_cities_fset.spatial_reference
In [12]:
re_ordered_stops_cities = list(map(lambda x: x.attributes['NAME'], re_ordered_stops_cities_fset))
print(re_ordered_stops_cities)
['Miami', 'Los Angeles', 'San Diego', 'San Jose', 'Phoenix', 'New Orleans', 'Orlando', 'El Paso', 'Houston', 'San Francisco']

Methods

The ArcGIS API for Python provides three ways to solve a routing problem, which are namely, RouteLayer.solve, find_routes, and plan_routes.

Operation network.analysis features.use_proximity
Route find_routes plan_routes
ServiceArea generate_service_areas create_drive_time_areas
ClosestFacility find_closest_facilities find_nearest

These three methods are defined in different modules of the arcgis package, and will make distinct REST calls in the back end. A key distinction between RouteLayer.solve and network.analysis.find_routes, features.use_proximity.plan_routes is, the former is meant for custom or advanced routing workflows where you need to publish your own Network Analysis layers. The latter tools work against routing services hosted on ArcGIS Online or available on your Enterprise via proxy services and will cost you credits.

In this part of guide, we will walk through the workflows of using network.analysis.find_routes() and features.use_proximity.plan_routes() in solving the same problem - designing the quickest route to travel amongst the ten cities defined in stops_cities, and further explore the differences in the process.

Method 1 - Using arcgis.network.analysis.find_routes

Finding a route analysis can mean determining the quickest or shortest way to travel between locations. You might want to use the find_routes tool in arcgis.network.analysis module to generate driving directions to visit multiple stops or to measure the distance or travel time between locations. The tool is capable of finding routes for one or more vehicles each time it runs, so you can determine the best routes for several drivers to visit pre-assigned stops. Parameters for find_routes include:

  • stops: Required parameter. Specify two or more stops to route between;
  • preserve_terminal_stops: Preserve Terminal Stops (string). Optional parameter. When Reorder Stops to Find Optimal Routes is checked (or True), you have options to preserve the starting or ending stops and the tool can reorder the rest.
  • time_of_day: Time of Day (datetime). Optional parameter. Specifies the time and date at which the routes should begin;
  • time_zone_for_time_of_day: Time Zone for Time of Day (str). Optional parameter. Specifies the time zone of the Time of Day parameter.
  • set save_output_na_layer to True, if you want to save the output route into a new layer file.
In [48]:
%%time

start_time = int(dt.datetime.now().timestamp() * 1000)

result = network.analysis.find_routes(re_ordered_stops_cities_fset, time_of_day=start_time, 
                                      time_zone_for_time_of_day="UTC",
                                      preserve_terminal_stops="Preserve None",
                                      reorder_stops_to_find_optimal_routes=True,
                                      save_output_na_layer=True)
WARNING 030194: Data values longer than 500 characters for field [Stops:Name] are truncated.
Network elements with avoid-restrictions are traversed in the output (restriction attribute names: "Through Traffic Prohibited").
Wall time: 1min 26s

Then, let's check if the tool has been successfully run, what are the types of each output element, and the network analysis layer URL of the output route.

In [49]:
print("Is the tool executed successfully?", result.solve_succeeded)
Is the tool executed successfully? True

The result we get from the previous operation is a tool output that contains three components - output_routes, output_stops, and output_directions.

In [50]:
type(result)
Out[50]:
arcgis.geoprocessing._support.ToolOutput
In [51]:
result.output_routes, result.output_stops, result.output_directions
Out[51]:
(<FeatureSet> 1 features, <FeatureSet> 10 features, <FeatureSet> 267 features)
In [52]:
result.output_network_analysis_layer.url
Out[52]:
'https://logistics.arcgis.com/arcgis/rest/directories/arcgisjobs/world/route_gpserver/je18da4dfbc64451badfb629433d3ef47/scratch/_ags_gpnac76d06923d674a76ad5524c4dcc726d5.lyr'

Tabularizing the response from find_routes

The output_directions component of the result can be read as a pandas data frame (which lists all the directions on the route).

In [53]:
""" Access the output directions
"""
df = result.output_directions.sdf
df = df[["RouteName", "ArriveTime", "DriveDistance", "ElapsedTime", "Text"]].loc[df["RouteName"] == "Miami - San Francisco"]
df.head()
Out[53]:
RouteName ArriveTime DriveDistance ElapsedTime Text
0 Miami - San Francisco 2019-10-29 23:30:30.365000010 0.000000 0.000000 Start at Miami
1 Miami - San Francisco 2019-10-29 23:30:30.365000010 0.089587 0.320852 Go west on SW 13th St toward SW 17th Ave
2 Miami - San Francisco 2019-10-29 23:30:49.615999937 1.322843 2.644720 Turn right on SW 17th Ave
3 Miami - San Francisco 2019-10-29 23:33:28.299000025 0.475841 1.502659 Turn right on NW 7th St (Luis Sabines Way)
4 Miami - San Francisco 2019-10-29 23:34:58.459000111 0.278966 0.669519 Turn left on NW 12th Ave (SR-933)
In [54]:
df.tail()
Out[54]:
RouteName ArriveTime DriveDistance ElapsedTime Text
262 Miami - San Francisco 2019-11-01 00:15:26.493999958 0.040609 0.166500 Continue forward on Masonic Ave
263 Miami - San Francisco 2019-11-01 00:15:36.483999968 0.059725 0.228120 Make a sharp right on Upper Ter
264 Miami - San Francisco 2019-11-01 00:15:50.171999931 0.067130 0.585009 Bear left
265 Miami - San Francisco 2019-11-01 00:15:50.171999931 0.000000 0.000000 Restriction: Through Traffic Prohibited
266 Miami - San Francisco 2019-11-01 00:16:25.272000074 0.000000 0.000000 Finish at San Francisco, on the left

Also, from the output_routes component of the returned object, we can then form a summary table of the entire trip.

In [55]:
""" Access output routes
"""
df = result.output_routes.sdf
start_times = pd.to_datetime(df["StartTime"], unit="ms")
end_times = pd.to_datetime(df["EndTime"], unit="ms")
df["StartTime"] = start_times.apply(lambda x: x.strftime("%H:%M:%S"))
df["EndTime"] = end_times.apply(lambda x: x.strftime("%H:%M:%S"))
df[["Name", "StartTime", "EndTime", "StopCount", "Total_Kilometers", "Total_Minutes"]]
Out[55]:
Name StartTime EndTime StopCount Total_Kilometers Total_Minutes
0 Miami - San Francisco 23:30:30 00:16:25 10 5279.728126 3105.915118

Based on the results shown above, the estimated travel distance from Miami to San Francisco is 5279.728 KM, and the estimated travel time is 3105.915 minutes (around 50.825 hours), which means that, if you start from Miami at 23:30:30 PM (EST) on Monday, for instance, you can expect to arrive at San Francisco at 00:16:25 PM (PST) of the same Wednesday.

Visualizing the response from find_routes

Now that we have fully mastered the itineraries of the trip, it is time to visualize the route on the map widget so target users can have a better grasp of the geographical relations between stops.

The following cell defines the styles and symbologies for the stops, and routes. In order to emphasize on the starting and ending stops, we will assign balloon icons as their symbols.

In [20]:
""" draw the route and stops with customized styles, symbologies, and popups
"""
styles = [    
    dict(selector="td", props=[("padding", "2px")]),
    dict(selector='.row_heading, .blank', props=[('display', 'none;')]),
    dict(selector='.col_heading, .blank', props=[('display', 'none;')])]

route_symbol = {
                    "type": "esriSLS",
                    "style": "esriSLSSolid",
                    "color": [128,0,128,90],
                    "width": 4
                }

stops_symbol = {"angle":0,"xoffset":2,"yoffset":8,"type":"esriPMS",
                "url":"http://static.arcgis.com/images/Symbols/Basic/ShinyPin.png",
                "contentType":"image/png","width":24,"height":24}

start_symbol = {"angle":0,"xoffset":0,"yoffset":8.15625,"type":"esriPMS",
                "url":"http://static.arcgis.com/images/Symbols/AtoZ/redA.png",
                "contentType":"image/png","width":15.75,"height":21.75}

end_symbol = {"angle":0,"xoffset":0,"yoffset":8.15625,"type":"esriPMS",
              "url":"http://static.arcgis.com/images/Symbols/AtoZ/greenB.png",
              "contentType":"image/png","width":15.75,"height":21.75}

popup_route = {"title": "Route", 
               "content": df.style.set_table_styles(styles).render()}
popup_stop = {"title": "Stop {}", 
              "content": df.style.set_table_styles(styles).render()}

Here, we will use the function check_curb_approach2 to determine if the stop is the start (the departing point), a by-passing point, or the end (the arriving point) of the route.

In [21]:
def check_curb_approach2(result):
    attributes = result.attributes
    return (attributes['ArriveCurbApproach'], attributes['DepartCurbApproach'])
In [62]:
map1 = my_gis.map('Texas, USA', zoomlevel=4)
map1
In [42]:
""" draw the route and stops with customized styles, symbologies, and popups
"""
for route in result.output_routes:
    map1.draw(route.geometry, popup_route, route_symbol)

sequence = 1
for stop in result.output_stops:
    
    stop_bool_tuple = check_curb_approach2(stop)
    if stop_bool_tuple[0] is None:
        # starting point
        symbol = start_symbol
    elif stop_bool_tuple[1] is None:
        # ending point
        symbol = end_symbol
    else:
        # by-passing point
        symbol = stops_symbol
        
    address = geocoding.reverse_geocode(stop.geometry)['address']['Match_addr']
    map1.draw(stop.geometry, 
              {"title": "Stop {}".format(sequence), 
               "content": address},
              symbol)
    sequence+=1
In [43]:
#save into a web map
item_properties = {
    "title": "Miami - San Francisco (2)",
    "tags" : "Routing",
    "snippet": " Route from Miami to San Francisco",
    "description": "a web map of Route from Miami to San Francisco using network.RouteLayer.solve"
}

item = map1.save(item_properties)
In [44]:
item
Out[44]:
Miami - San Francisco (2)
Route from Miami to San FranciscoWeb Map by portaladmin
Last Modified: September 18, 2019
0 comments, 0 views

We have been using the find_routes tool in the network.analysis module up to this point. From now on, let's use a different method - plan_routes - defined in the features.use_proximity module, to achieve a workflow driven, Feature Service to Feature Service user experience.

Method 2 - Using arcgis.features.use_proximity.plan_routes

The plan_routes method determines how to efficiently divide tasks among a mobile workforce.

You provide the input, which includes a set of stops and the number of vehicles available to visit the stops, and the tool assigns the stops to vehicles and returns routes showing how each vehicle can reach their assigned stops in the least amount of time.

With plan_routes, mobile workforces reach more job sites in less time, which increases productivity and improves customer service. Organizations often use plan_routes to:

  • Inspect homes, restaurants, and construction sites
  • Provide repair, installation, and technical services
  • Deliver items and small packages
  • Make sales calls
  • Provide van transportation from spectators’ homes to events

For more information, please refer to the API help doc.

Prepare the input parameters

Because the stops_layer is required to be of a Feature Layer, we need to prepare the input data in a different format.

In [13]:
re_ordered_values = "'" + "', '".join(re_ordered_stops_cities) + "'"
re_ordered_values
Out[13]:
"'Miami', 'Los Angeles', 'San Diego', 'San Jose', 'Phoenix', 'New Orleans', 'Orlando', 'El Paso', 'Houston', 'San Francisco'"
In [14]:
stops_layer = {'url': sample_cities.layers[0].url, 
               'filter': "ST in ('CA', 'NV', 'TX', 'AZ', 'LA', 'FL')  AND NAME IN ({0})".format(re_ordered_values)}
start_layer = {'url': sample_cities.layers[0].url, 
               'filter': "ST = 'FL' AND NAME = 'Miami'"}
end_layer = {'url': sample_cities.layers[0].url, 
             'filter': "NAME = 'San Francisco'"}

With output_name specified

A reference to the output Feature Layer Collection is returned when output_name is specified. And the returned Feature Layer includes a layer of routes showing the shortest paths to visit the stops; a layer of the stops assigned to routes, as well as any stops that couldn’t be reached due to the given parameter settings; and a layer of directions containing the travel itinerary for each route.

In [16]:
%%time

""" using https://analysis7.arcgis.com/arcgis/rest/services/tasks/GPServer/PlanRoutes/submitJob
"""
result1 = use_proximity.plan_routes(stops_layer=stops_layer, route_count=1, 
                                    max_stops_per_route=10, route_start_time=start_time,
                                    start_layer_route_id_field = "FID",
                                    start_layer=start_layer, 
                                    end_layer=end_layer,
                                    return_to_start=False,
                                    context={'outSR': {"wkid": 4326}},
                                    output_name="Plan Route from Miami to San Francisco 2a",
                                    gis=my_gis)
Network elements with avoid-restrictions are traversed in the output (restriction attribute names: "Through Traffic Prohibited").
Wall time: 1min 59s
In [17]:
result1
Out[17]:
Plan Route from Miami to San Francisco 2a
Feature Layer Collection by arcgis_python
Last Modified: October 29, 2019
0 comments, 0 views
In [18]:
route_sublayer = FeatureLayer.fromitem(result1, layer_id=1)
route_sublayer.url
Out[18]:
'https://services7.arcgis.com/JEwYeAy2cc8qOe3o/arcgis/rest/services/Plan Route from Miami to San Francisco 2a/FeatureServer/1'
In [19]:
route_sublayer.query(where='1=1', as_df=True)
Out[19]:
EndTime EndTimeUTC OBJECTID RouteLayerItemID RouteLayerItemURL RouteName SHAPE StartTime StartTimeUTC StopCount TotalStopServiceTime TotalTime TotalTravelTime Total_Kilometers Total_Miles
0 2019-11-01 03:50:24 2019-11-01 10:50:24 1 None None Miami - San Francisco - Route1 {'paths': [[[-8930153.5818, 2969499.7754], [-8... 2019-10-30 03:30:30 2019-10-30 07:30:30 10 0 3079.901315 3079.901315 5282.739 3282.549989

From this summary table, we can tell the estimated travel distance from Miami to San Francisco is 5282.739 KM, and the estimated travel time is 3079.901 minutes (around 51.332 hours).

In [22]:
map2a = my_gis.map('Texas, USA', zoomlevel=4)
map2a