Custom directions

Custom directions allow for the altering of the turn-by-turn directions text after it is initially generated during a solve. The existing directions for a route can be updated or removed, or new directions points can be added. This permits changing the output directions without the need to update the underlying network dataset.

Some example applications of custom directions are as follows:

  • Add a directions event when entering or exiting a toll road or when transferring from a surface street to a subway line
  • Alter directions maneuver text to emphasize sharp turn angles
  • Alert the driver after every 120 minutes of driving
  • Simplify or expand directions instructions for a specific application

Custom directions are implemented by creating a Python class that inherits from the arcpy.nax.DirectionsCustomizer and associating the Python class with a particular network dataset.

When implementing a directions customizer, there are several objects that can be used to gather information about the network and the directions, such as DirectionPoint, DirectionsName, ReferenceLandmark, and others.

Custom directions class

A directions customizer is created by defining a class that inherits from the arcpy.nax.DirectionsCustomizer class and implements the customize method. The class can, optionally, implement the __init__ and attach methods.

Custom directions methods

The subsections below describe the methods that can be used to customize directions at solve time.

Initializer

It is optional to implement the initializer (__init__) method. This is the standard initializer for a Python object and can be used as needed.

Attach

Implementing the attach method is optional. The method can be used to inspect and validate the network dataset to ensure it complies with the requirements of the custom directions code using the network query object that is passed into this method. If the network dataset is valid, the attach method should return True; otherwise, it should return False, in which case the directions customizer will not be associated with the network dataset and no other class methods will be invoked.

For temporary directions customizers, the attach method is invoked when the directionsCustomizer property on a network dataset object in a script is assigned.

For persisted custom directions, the attach method is invoked internally by core network dataset code when a network dataset is opened.

Note:

A network dataset can be opened multiple times depending on the number of threads the application is using to access a network dataset.

Customize

Implementing the customize method is required. This method will be invoked after the initial directions are created and where directions can be altered.

The arcpy.nax.DirectionsQuery object that is passed into this method can be used to iterate through the junctions and the edges to get their associated directions points.

The existing direction points can be updated, such as altering the display text. If needed, new direction points can be created and added to an element's list of direction points. Alternatively, existing direction points can be removed.

Note:

  • When creating a direction point, at minimum the displayText and the directionPointType properties should be set.
  • The properties azimuth, arrivalTime, and exitNumber are set internally and cannot be set in a directions customizer.

The elements have various properties that might be helpful when customizing a route's directions. The self.networkQuery object can be used to retrieve information from the network dataset.

A route is made up of stops, edges, and junctions. These feature classes are used to create a route's directions. The directions themselves are made up of direction points, and direction lines feature classes. Directions are created by analyzing the stops, edges, and junctions to find locations where something occurs that a user should be informed about. Direction points are then created at these locations and associated with the appropriate element. Usually, direction points are associated with junctions. Currently, direction points are only associated with edges in the case of where a landmark is encountered. The direction lines are then created by appending together the edges between direction points.

Note:

There can be more than one direction point associated with the same feature.

Examples

Example 1:The code below is a simple example of a custom directions class that adds a number to the beginning of each directions maneuver.

import arcpy

class DirectionsIndexer(arcpy.nax.DirectionsCustomizer):
    """Defines a directions customizer that adds and index number to directions."""

    def customize(self, directions_query: arcpy.nax.DirectionsQuery):
        """Add an index number to each directions maneuver."""
        index = 1
        for junction in directions_query.junctions:
            for point in junction.directionPoints:
                point.displayText = f"{index} -> {point.displayText}"
                index += 1

Example 2: The code below shows a custom directions class with the optional methods implemented.

import arcpy

class DirectionsIndexer(arcpy.nax.DirectionsCustomizer):
    """Defines a directions customizer that adds and index number to directions."""

    def __init__(self):
        """Example initializer."""
        super().__init__()
        # Do additional custom initialization

    def attach(self, network_query: arcpy.nax.NetworkQuery) -> bool:
        # Do additional validation checks before returning Boolean
        return True

    def customize(self, directions_query: arcpy.nax.DirectionsQuery):
        """Add an index number to each directions maneuver."""
        index = 1
        for junction in directions_query.junctions:
            for point in junction.directionPoints:
                point.displayText = f"{index} -> {point.displayText}"
                index += 1

Example 3: The code below shows a custom directions class that adds new directions points with a custom message to alert the driver when they've been driving for more than two hours.

import arcpy

class LongDriveAlert(arcpy.nax.DirectionsCustomizer):
    """Defines a directions customizer that reports an alert after a long drive."""

    def customize(self, directions_query: arcpy.nax.DirectionsQuery) -> None:
        """Customize directions.

        If traversed junction's accumulated time is over alert interval, add an alert as a directions point.
        Enhance existing directions text to show time elapsed since beginning of the trip.
        """
        alert_interval = 120  # This assumes the travel mode's time attribute unit is minutes.
        # Loop through traversed junctions and get accumulated time at each junction.
        # If the accumulated time is over the alert interval, add a directions point with an alert message.
        i = 1
        for junction in directions_query.junctions:
            elapsed_time = junction.accumulatedTime
            if elapsed_time > alert_interval * i:
                # Create a new directions point at junction.
                point = arcpy.nax.DirectionPoint(junction)
                point.displayText = f"You have driven for over 2 hours. Consider taking a break."
                point.directionPointType = arcpy.nax.DirectionPointType.Event
                # Update junction direction points to add the new break reminder
                junction.directionPoints.append(point)
                i += 1
            else:
                for point in junction.directionPoints:
                    # For existing directions, report the elapsed time at each direction point.
                    point.displayText = f"{point.displayText} (Time elapsed: {elapsed_time:.2f} minutes)"

Example 4: The code below shows a custom directions class that inspects the direction's point type to identify left turns and adds additional warning text to the directions instructions.

import arcpy

class LeftTurnsHighlighter(arcpy.nax.DirectionsCustomizer):
    """Add warning text for left turn maneuvers."""

    def __init__(self) -> None:
        """Initialize customizer.

        Set left_turn_maneuver_types in initialization so it is set only once
        when the network dataset object is constructed and when the
        customizer instance is initialized.
        """
        super().__init__()
        self.left_turn_manuever_types = [
            arcpy.nax.DirectionPointType.ManeuverForkLeft,
            arcpy.nax.DirectionPointType.ManeuverRampLeft,
            arcpy.nax.DirectionPointType.ManeuverUTurnLeft,
            arcpy.nax.DirectionPointType.ManeuverBearLeft,
            arcpy.nax.DirectionPointType.ManeuverTurnLeft,
            arcpy.nax.DirectionPointType.ManeuverSharpLeft,
            arcpy.nax.DirectionPointType.ManeuverTurnLeftLeft,
            arcpy.nax.DirectionPointType.ManeuverTurnLeftRight,
            arcpy.nax.DirectionPointType.ManeuverTurnRightLeft
        ]

    def customize(self, directions_query: arcpy.nax.DirectionsQuery) -> None:
        """Alter directions text to highlight left turns with warning text."""
        for junction in directions_query.junctions:
            for point in junction.directionPoints:
                if point.directionPointType in self.left_turn_manuever_types:
                    point.displayText = f"{point.displayText} (LEFT TURN WARNING!)"

Associate a directions customizer with a network dataset

There are two ways to deploy a directions customizer so that it is associated with a network dataset and has the customization logic invoked during a solve. These two methods are referred to as temporary and persisted.

Tip:

Create your custom directions class and test it as a temporary directions customizer. Then run the script in debug mode in an editor such as Visual Studio Code. Once the script is working as expected, and if required, you can make it a persisted directions customizer. Validate the persisted directions customizer to ensure everything is working correctly.

Temporary directions customizer

Temporary directions customizers are only associated with a network dataset object created within a script; they are not saved permanently to the network dataset. Temporary directions customizers are configured using the directionsCustomizer property on a network dataset object within a script that performs a solve.

A temporary directions customizer can be used for applications in which the directions customizer must be invoked from a Python script, for example, a stand-alone Python script, a Python script tool, or a custom geoprocessing service or a web tool. They can also be useful for developing and debugging of the persisted directions customizers.

Configure a temporary directions customizer

To set up a temporary directions customizer, create a directions customizer object, then use the directionsCustomizer property on a network dataset object to associate the directions customizer object with it.

The example below shows how to instantiate a custom directions object and associate it to the network dataset object. To invoke the custom directions at solve time, instantiate a route solver object using the network dataset object.

# Instantiate a directions customizer object that adds an index number
# to each directions maneuver's display text
add_numbers_customizer = DirectionsIndexer()

# Create a network dataset object
network_dataset = arcpy.nax.NetworkDataset(
    r"C:\Data\Tutorial\SanFrancisco.gdb\Transportation\Streets_ND")

# Attach the custom directions object to the network dataset
network_dataset.directionsCustomizer = add_numbers_customizer

# Instantiate a route analysis
route = arcpy.nax.Route(network_dataset)

Persisted directions customizer

Persisted directions customizers store a reference to a directions customizer class as part of the network dataset schema, which is stored in the geodatabase. These directions customizers will be invoked whenever a solve is done using that network dataset. This is referred to as persisted since the reference is part of the network dataset itself. They are configured using the updateNetworkDatasetSchema method on a network dataset object.

When a network with a persisted directions customizer is opened, the directions customizer class is loaded and cached. This cache is retained for the lifetime of the application. This means that any changes made to a persisted class will not be read until the application that opened the network dataset is closed and restarted. This applies to both ArcGIS Pro and ArcGIS Server.

It is important to note that for persisted directions customizers, the network dataset’s schema only contains a reference to a directions customizer, it does not contain the class code. This reference allows the network dataset, when it is accessed, to implicitly find and load its referenced directions customizer. A Python module containing the class code must reside in the active ArcGIS Pro Python environment's site-packages folder so that the network dataset can find it.

Learn more about Python environments

Note:

It is recommended that you clone the default ArcGIS Pro Python environment before making any modifications. When adding a custom directions Python file to the site packages directory, it is recommended that you first clone the environment and activate the cloned environment..

A persisted directions customizer would be used when you need to have a directions customizer invoked when doing a solve outside of a Python script, such as in ArcGIS Pro or ArcGIS Server. For example, to create a network analysis layer in ArcGIS Pro, solve the layer and generate directions using your customization, or if you want to use standard routing services published based on a network dataset, but want to customize the directions output.

If a network analyst layer using a network dataset that has a persisted directions customizer is published as a service, the custom directions package must be manually copied to the ArcGIS Server Python environment's site packages directory. Also, when a directions customizer is used on a service, any external resource it uses (such as files) should be accessible by the ArcGIS Server user, as it is dependent on how the server is configured.

Configure a persisted directions customizer

Use the updateNetworkDatasetSchema method on a network dataset object to permanently update the network dataset schema, passing in a dictionary that defines the path to the directions customizer class. The path uses the dot notation to define the folder name (within the site-packages directory), the file name the class is in, and the class name itself.

The example below shows how to update a network dataset with a persisted custom directions class. The class in this example is called DirectionsIndexer, and its code is in a Python module called customization.py in a folder called na_customizers, which is in the site-packages folder of an ArcGIS Pro active Python environment.

import arcpy

# Create a network dataset object
network_dataset = arcpy.nax.NetworkDataset(
    r"C:\Data\Tutorial\SanFrancisco_Persisted.gdb\Transportation\Streets_ND")

# Create a dictionary referencing the custom directions class to use
my_custom_directions = {"class": "na_customizers.customization.DirectionsIndexer"}

# Update the network dataset to use the custom directions class
network_dataset.updateNetworkDatasetSchema(
    custom_directions=my_custom_directions
)

When this network dataset is used in a network analysis, an object of the directions customizer class will be created, and the network dataset will invoke the directions customizer object after generating the initial directions to customize the directions. When creating the object of the directions customizer, the network will find and load the specified class within the package and module from the active ArcGIS Pro environment's site packages folder. If package, module, or class is not found, the directions customizer will not be used. The solve and direction generation will complete with a warning message stating that it has had an issue using the directions customizer

When a network dataset has a persisted directions customizer, it will be listed in the General > Summary section and on the Directions page of the Network Dataset Properties dialog box. If there was an issue loading the referenced class, an error or warning message appears.

Object lifecycle

When a network dataset is initially constructed, and it has a persisted directions customizer, it instantiates a directions customizer object that is referenced throughout its lifetime, where the lifetime can vary depending on the framework being used (such as ArcGIS Pro, ArcGIS Server, or Python).

Since a specific instance of a directions customizer object could be used over multiple solves, it is important to manage the state of this object, in particular resetting variables at the beginning of the Customize method before iterating through the current directions as needed. For example, if the directions customizer has an instance variable that is updated while iterating over the directions, it should be reset at the beginning of the Customize method; otherwise, it might start with a value from the previous solve.

In the context of ArcGIS Server, each SOC process (at startup time and at recycle time) will construct a new network dataset object, and will also create a new instance of directions customizer object. This instance of directions customizer object will be used throughout the lifecycle of the SOC process; only the Customize method will run per request.

Limitations

The following limitations only apply to custom directions:

  • Custom directions are only available in ArcGIS Pro and ArcGIS Server.
  • Custom directions can only be invoked on a file or enterprise geodatabase.
  • For performance reasons, directions customizers do not support using keywords for arguments.

Quick start guide to create and use a temporary directions customizer

The following sections serve as a quick guide describing how to create and use a temporary directions customizer. Each code sample illustrates a specific component of the full workflow. The components are as follows:

  1. Solve a route analysis
  2. Define the directions customizer class
  3. Create an instance of the class and associate it with the network dataset that is used for the route solve

The final code sample shows how to put all the components together.

The code samples are created using the network analyst tutorial that is available for download from the data download page.

Solve a route analysis

The code sample below illustrates a workflow to solve a route analysis using the arcpy.nax solver object and print turn-by-turn directions.

Note:

The path to the gdb in the code below must be updated to reflect where the data is on your system.

import arcpy

# Create a network dataset object
network_dataset = arcpy.nax.NetworkDataset(
    r"C:\Data\Tutorial\SanFrancisco.gdb\Transportation\Streets_ND")

# Instantiate a route analysis
route = arcpy.nax.Route(network_dataset)
route.returnDirections = True

# Insert stops for the route
with route.insertCursor(
    arcpy.nax.RouteInputDataType.Stops,
    ["NAME", "SHAPE@XY"]
) as cursor:
    cursor.insertRow(["Stop1", (-122.501, 37.757)])
    cursor.insertRow(["Stop2", (-122.445, 37.767)])

# Solve the route
result = route.solve()

# Print the directions
if result.solveSucceeded:
    for row in result.searchCursor(
        arcpy.nax.RouteOutputDataType.DirectionPoints, ["DisplayText"]
    ):
        print(row[0])

Define a directions customizer class

The code sample below illustrates a directions customizer class definition. This example adds a number at the start of each directions maneuver. For example, without customization, the first direction would be Start at Stop1, with customization, the first direction would be 1 -> Start at Stop1.

import arcpy

class DirectionsIndexer(arcpy.nax.DirectionsCustomizer):
    """Defines a directions customizer that adds and index number to directions."""

    def customize(self, directions_query: arcpy.nax.DirectionsQuery):
        """Add an index number to each directions maneuver."""
        index = 1
        for junction in directions_query.junctions:
            for point in junction.directionPoints:
                point.displayText = f"{index} -> {point.displayText}"
                index += 1

Associate a directions customizer with a network dataset

The code sample below illustrates creating an instance of the directions customizer class and associating it with the network dataset object. This should be done before the route solve is invoked (route.solve()).

# Instantiate a directions customizer object that adds an index number
# to each directions maneuver's display text
add_numbers_customizer = DirectionsIndexer()

# Attach the custom directions object to the network dataset
network_dataset.directionsCustomizer = add_numbers_customizer

Combine all the components

The code sample below shows how to put all the components together into a complete workflow that defines and uses a temporary directions customizer for a route analysis workflow.

import arcpy

class DirectionsIndexer(arcpy.nax.DirectionsCustomizer):
    """Defines a directions customizer that adds and index number to directions."""

    def customize(self, directions_query: arcpy.nax.DirectionsQuery):
        """Add an index number to each directions maneuver."""
        index = 1
        for junction in directions_query.junctions:
            for point in junction.directionPoints:
                point.displayText = f"{index} -> {point.displayText}"
                index += 1

# Create a network dataset object
network_dataset = arcpy.nax.NetworkDataset(
    r"C:\Data\Tutorial\SanFrancisco.gdb\Transportation\Streets_ND")

# Instantiate a directions customizer object that adds an index number
# to each directions maneuver's display text
add_numbers_customizer = DirectionsIndexer()

# Attach the custom directions object to the network dataset
network_dataset.directionsCustomizer = add_numbers_customizer

# Instantiate a route analysis
route = arcpy.nax.Route(network_dataset)
route.returnDirections = True

# Insert stops for the route
with route.insertCursor(
    arcpy.nax.RouteInputDataType.Stops,
    ["NAME", "SHAPE@XY"]
) as cursor:
    cursor.insertRow(["Stop1", (-122.501, 37.757)])
    cursor.insertRow(["Stop2", (-122.445, 37.767)])

# Solve the route
result = route.solve()

# Print the directions
if result.solveSucceeded:
    for row in result.searchCursor(
        arcpy.nax.RouteOutputDataType.DirectionPoints, ["DisplayText"]
    ):
        print(row[0])

Quick start guide to create and use a persisted directions customizer

The following sections serve as a quick guide describing how to create and use a persisted directions customizer. Below, you will create a Python module that contains a directions customizer class, store the Python module in the active Python environment ’s site-packages directory, update the network dataset to use the directions customizer, and test it by solving a route analysis.

  1. Clone the default Python environment
  2. Define the directions customizer class
  3. Update the network dataset schema
  4. Solve the route

The code samples are created using the network analyst tutorial that is available for download from the data download page.

Clone the default Python environment

The directions customizer code must be saved to the ArcGIS Pro Python environment. If you are using the default ArcGIS Pro Python environment, it is recommended that you first clone and activate a new environment.

Learn more about cloning an environment

Define the directions customizer class

The code sample below illustrates a directions customizer class definition. This example adds a number at the start of each directions maneuver.

import arcpy

class DirectionsIndexer(arcpy.nax.DirectionsCustomizer):
    """Defines a directions customizer that adds and index number to directions."""

    def customize(self, directions_query: arcpy.nax.DirectionsQuery):
        """Add an index number to each directions maneuver."""
        index = 1
        for junction in directions_query.junctions:
            for point in junction.directionPoints:
                point.displayText = f"{index} -> {point.displayText}"
                index += 1

Within the active ArcGIS Pro Python environment, find the site-packages folder. Within that directory, create a folder called na_customizers. Save the above code defining a directions customizer class to the na_customizers folder as direction_customization.py.

Update the network dataset schema

Copy Network Analyst\Tutorial\SanFrancisco.gdb from the tutorial data to SanFrancisco_Persisted.gdb.

Use the code below in a stand-alone script to permanently update the network dataset in SanFrancisco_Persisted.gdb with a persisted custom directions class.

import arcpy

# Create a network dataset object
network_dataset = arcpy.nax.NetworkDataset(
    r"C:\Data\Tutorial\SanFrancisco_Persisted.gdb\Transportation\Streets_ND")

# Create a dictionary referencing the custom directions class to use
my_custom_directions = {
    {"class": "na_customizers.customization.DirectionsIndexer"}
}

# Update the network dataset to use the custom directions class
network_dataset.updateNetworkDatasetSchema(
    custom_directions=my_custom_directions
)

Solve route

In ArcGIS Pro, use the network dataset from the SanFrancisco_Persisted.gdb to solve a route. Solve a second route, using SanFrancisco.gdb with the same stops and compare the output route directions. The directions in the route referencing SanFrancisco_Persisted.gdb should have a number prefixed to each of the directions because of the directions customizer. For example, using SanFrancisco.gdb, the first direction would be Start at Stop1, whereas using SanFrancisco_Persisted.gdb, the first direction would be 1 -> Start at Stop1.

Learn how to solve a route analysis in ArcGIS Pro