Skip to content

Commit

Permalink
Add the option to reverse direction of travel
Browse files Browse the repository at this point in the history
  • Loading branch information
mmorang committed Apr 2, 2022
1 parent c29685a commit 53ddfee
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 18 deletions.
14 changes: 13 additions & 1 deletion LargeNetworkAnalysisTools.pyt
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,16 @@ class SolveLargeAnalysisWithKnownPairs(object):
)
param_sort_origins.value = True

param_reverse_direction = arcpy.Parameter(
displayName="Reverse Direction of Travel",
name="Reverse_Direction",
datatype="GPBoolean",
parameterType="Optional",
direction="Input",
category="Advanced"
)
param_reverse_direction.value = False

params = [
param_origins, # 0
param_origin_id_field, # 1
Expand All @@ -510,6 +520,7 @@ class SolveLargeAnalysisWithKnownPairs(object):
param_barriers, # 13
param_precalculate_network_locations, # 14
param_sort_origins, # 15
param_reverse_direction # 16
]

return params
Expand Down Expand Up @@ -563,7 +574,8 @@ class SolveLargeAnalysisWithKnownPairs(object):
time_of_day, # time of day
get_catalog_path_multivalue(parameters[13]), # barriers
parameters[14].value, # Should precalculate network locations
parameters[15].value # Should sort origins
parameters[15].value, # Should sort origins
parameters[16].value # Reverse direction of travel
)

# Solve the OD Cost Matrix analysis
Expand Down
45 changes: 36 additions & 9 deletions parallel_route_pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import datetime
import traceback
import argparse
from distutils.util import strtobool

import arcpy

Expand Down Expand Up @@ -89,6 +90,7 @@ def __init__(self, **kwargs):
self.time_units = kwargs["time_units"]
self.distance_units = kwargs["distance_units"]
self.time_of_day = kwargs["time_of_day"]
self.reverse_direction = kwargs["reverse_direction"]
self.scratch_folder = kwargs["scratch_folder"]
self.barriers = []
if "barriers" in kwargs:
Expand Down Expand Up @@ -248,11 +250,11 @@ def _insert_stops(self):
location_fields
) as icur:
# Loop through origins and insert them into Stops along with their assigned destinations
for origin_row in arcpy.da.SearchCursor( # pylint: disable=no-member
for origin in arcpy.da.SearchCursor( # pylint: disable=no-member
self.input_origins_layer,
["SHAPE@", self.origin_id_field, self.assigned_dest_field]
):
dest_id = origin_row[2]
dest_id = origin[2]
if dest_id is None:
continue
if dest_id not in destinations:
Expand All @@ -269,13 +271,25 @@ def _insert_stops(self):
continue
# Insert origin and destination
destination_row = destinations[dest_id]
route_name = f"{origin_row[1]} - {dest_id}"
origin_row = [route_name, 1, origin_row[1], origin_row[0], None]
if self.reverse_direction:
route_name = f"{dest_id} - {origin[1]}"
origin_sequence = 2
destination_sequence = 1
else:
route_name = f"{origin[1]} - {dest_id}"
origin_sequence = 1
destination_sequence = 2
origin_row = [route_name, origin_sequence, origin[1], origin[0], None]
if location_fields:
# Include invalid location fields as a placeholder so they'll be calculated at solve time
origin_row += [-1, -1, -1, -1]
icur.insertRow(origin_row)
icur.insertRow([route_name, 2, None] + list(destination_row))
destination_row = [route_name, destination_sequence, None] + list(destination_row)
if self.reverse_direction:
icur.insertRow(destination_row)
icur.insertRow(origin_row)
else:
icur.insertRow(origin_row)
icur.insertRow(destination_row)

def solve(self, origins_criteria): # pylint: disable=too-many-locals, too-many-statements
"""Create and solve an Route analysis for the designated chunk of origins and their assigned destinations.
Expand Down Expand Up @@ -367,15 +381,21 @@ def _export_to_feature_class(self, origins_criteria):
self.solve_result.export(arcpy.nax.RouteOutputDataType.Stops, output_stops)

# Join the input ID fields to Routes
if self.reverse_direction:
first_stop_field = self.dest_unique_id_field_name
second_stop_field = self.origin_unique_id_field_name
else:
first_stop_field = self.origin_unique_id_field_name
second_stop_field = self.dest_unique_id_field_name
helpers.run_gp_tool(
self.logger,
arcpy.management.JoinField,
[output_routes, "FirstStopOID", output_stops, "ObjectID", [self.origin_unique_id_field_name]]
[output_routes, "FirstStopOID", output_stops, "ObjectID", [first_stop_field]]
)
helpers.run_gp_tool(
self.logger,
arcpy.management.JoinField,
[output_routes, "LastStopOID", output_stops, "ObjectID", [self.dest_unique_id_field_name]]
[output_routes, "LastStopOID", output_stops, "ObjectID", [second_stop_field]]
)

self.job_result["outputRoutes"] = output_routes
Expand Down Expand Up @@ -418,7 +438,7 @@ class ParallelRoutePairCalculator:
def __init__( # pylint: disable=too-many-locals, too-many-arguments
self, origins, origin_id_field, assigned_dest_field, destinations, dest_id_field,
network_data_source, travel_mode, time_units, distance_units,
max_routes, max_processes, out_routes, scratch_folder, time_of_day=None, barriers=None
max_routes, max_processes, out_routes, reverse_direction, scratch_folder, time_of_day=None, barriers=None
):
"""Compute Routes between origins and their assigned destinations in parallel and combine results.
TODO
Expand Down Expand Up @@ -465,6 +485,7 @@ def __init__( # pylint: disable=too-many-locals, too-many-arguments
"time_units": time_units,
"distance_units": distance_units,
"time_of_day": time_of_day,
"reverse_direction": reverse_direction,
"scratch_folder": self.scratch_folder,
"barriers": barriers
}
Expand Down Expand Up @@ -670,6 +691,12 @@ def launch_parallel_rt_pairs():
parser.add_argument(
"-mp", "--max-processes", action="store", dest="max_processes", type=int, help=help_string, required=True)

# --reverse-direction parameter
help_string = "Whether to reverse the direction of travel (destination to origin)."
parser.add_argument(
"-rd", "--reverse-direction", action="store", type=lambda x: bool(strtobool(x)),
dest="reverse_direction", help=help_string, required=True)

# --out-routes parameter
help_string = "The full catalog path to the output routes feature class."
parser.add_argument("-r", "--out-routes", action="store", dest="out_routes", help=help_string, required=True)
Expand Down
12 changes: 10 additions & 2 deletions solve_large_route_pair_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ class RoutePairSolver: # pylint: disable=too-many-instance-attributes, too-few-
def __init__( # pylint: disable=too-many-locals, too-many-arguments
self, origins, origin_id_field, assigned_dest_field, destinations, dest_id_field,
network_data_source, travel_mode, time_units, distance_units,
chunk_size, max_processes, output_routes,
time_of_day=None, barriers=None, precalculate_network_locations=True, sort_origins=True
chunk_size, max_processes, output_routes, time_of_day=None, barriers=None,
precalculate_network_locations=True, sort_origins=True, reverse_direction=False
):
"""Initialize the RoutePairSolver class.
Expand Down Expand Up @@ -99,6 +99,7 @@ def __init__( # pylint: disable=too-many-locals, too-many-arguments
self.barriers = barriers if barriers else []
self.should_precalc_network_locations = precalculate_network_locations
self.should_sort_origins = sort_origins
self.reverse_direction = reverse_direction
self.output_routes = output_routes

# Scratch folder to store intermediate outputs from the Route processes
Expand Down Expand Up @@ -372,6 +373,7 @@ def _execute_solve(self):
"--distance-units", self.distance_units,
"--max-routes", str(self.chunk_size),
"--max-processes", str(self.max_processes),
"--reverse-direction", str(self.reverse_direction),
"--out-routes", str(self.output_routes),
"--scratch-folder", self.scratch_folder
]
Expand Down Expand Up @@ -521,6 +523,12 @@ def _run_from_command_line():
"-so", "--sort-origins", action="store", type=lambda x: bool(strtobool(x)),
dest="sort_origins", help=help_string, required=True)

# --reverse-direction parameter
help_string = "Whether to reverse the direction of travel (destination to origin)."
parser.add_argument(
"-rd", "--reverse-direction", action="store", type=lambda x: bool(strtobool(x)),
dest="reverse_direction", help=help_string, required=True)

# Get arguments as dictionary.
args = vars(parser.parse_args())

Expand Down
11 changes: 7 additions & 4 deletions unittests/test_SolveLargeAnalysisWithKnownPairs_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ def test_run_tool(self):
datetime.datetime(2022, 3, 29, 16, 45, 0), # time of day
self.barriers, # barriers
True, # precalculate network locations
True # Sort origins
True, # Sort origins
False # Reverse direction of travel
)
# Check results
self.assertTrue(arcpy.Exists(out_routes))

def test_run_tool_service(self):
"""Test that the tool runs with a service as a network data source."""
"""Test that the tool runs with a service as a network data source. Use reverse order"""
out_routes = os.path.join(self.output_gdb, "OutRoutesService")
arcpy.LargeNetworkAnalysisTools.SolveLargeAnalysisWithKnownPairs( # pylint: disable=no-member
self.origins,
Expand All @@ -107,7 +108,8 @@ def test_run_tool_service(self):
None, # time of day
None, # barriers
False, # precalculate network locations
False # Sort origins
False, # Sort origins
True # Reverse direction of travel
)
# Check results
self.assertTrue(arcpy.Exists(out_routes))
Expand All @@ -133,7 +135,8 @@ def test_error_agol_max_processes(self):
None, # time of day
None, # barriers
True, # precalculate network locations
True # Sort origins
True, # Sort origins
False # Reverse direction of travel
)
expected_messages = [
"Failed to execute. Parameters are not valid.",
Expand Down
3 changes: 3 additions & 0 deletions unittests/test_parallel_route_pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def setUpClass(self): # pylint: disable=bad-classmethod-argument
"time_units": arcpy.nax.TimeUnits.Minutes,
"distance_units": arcpy.nax.DistanceUnits.Miles,
"time_of_day": None,
"reverse_direction": False,
"scratch_folder": self.output_folder,
"barriers": []
}
Expand All @@ -83,6 +84,7 @@ def setUpClass(self): # pylint: disable=bad-classmethod-argument
"max_routes": 15,
"max_processes": 4,
"out_routes": os.path.join(self.output_gdb, "OutRoutes"),
"reverse_direction": False,
"scratch_folder": self.output_folder, # Should be set within test if real output will be written
"time_of_day": "20220329 16:45",
"barriers": ""
Expand Down Expand Up @@ -162,6 +164,7 @@ def test_cli(self):
"--max-routes", "15",
"--max-processes", "4",
"--out-routes", os.path.join(self.output_gdb, "OutCLIRoutes"),
"--reverse-direction", "false",
"--scratch-folder", out_folder,
"--time-of-day", "20220329 16:45"
]
Expand Down
6 changes: 4 additions & 2 deletions unittests/test_solve_large_route_pair_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ def setUpClass(self): # pylint: disable=bad-classmethod-argument
"time_of_day": self.time_of_day_str,
"barriers": "",
"precalculate_network_locations": True,
"sort_origins": True
"sort_origins": True,
"reverse_direction": False
}

def test_validate_inputs(self):
Expand Down Expand Up @@ -192,7 +193,8 @@ def test_cli(self):
"--out-routes", os.path.join(self.output_gdb, "OutCLIRoutes"),
"--time-of-day", self.time_of_day_str,
"--precalculate-network-locations", "true",
"--sort-origins", "true"
"--sort-origins", "true",
"--reverse-direction", "false"
]
result = subprocess.run(rt_inputs, check=True)
self.assertEqual(result.returncode, 0)
Expand Down

0 comments on commit 53ddfee

Please sign in to comment.