raise # ----- Prepare OD service ----- try: arcpy.AddMessage("Obtaining credentials for and information about OD Cost Matrix service...") # Hard-wired OD variables TravelMode = "Walking Distance" PathShape = "None" OD_service_name = "World/OriginDestinationCostMatrix" Utility_service_name = "World/Utilities" # Get the credentials from the signed in user and import the service if username and password: ODservice = BBB_SharedFunctions.import_AGOLservice(OD_service_name, username=username, password=password) Utilityservice = BBB_SharedFunctions.import_AGOLservice(Utility_service_name, username=username, password=password) else: credentials = arcpy.GetSigninToken() if not credentials: arcpy.AddError("Please sign into ArcGIS Online or pass a username and password to the tool.") raise CustomError token = credentials["token"] referer = credentials["referer"] ODservice = BBB_SharedFunctions.import_AGOLservice(OD_service_name, token=token, referer=referer) Utilityservice = BBB_SharedFunctions.import_AGOLservice(Utility_service_name, token=token, referer=referer) # Get the service limits from the OD service (how many origins and destinations allowed) utilresult = Utilityservice.GetToolInfo("asyncODCostMatrix", "GenerateOriginDestinationCostMatrix") utilresultstring = utilresult.getOutput(0) utilresultjson = json.loads(utilresultstring)
def runTool(outFile, SQLDbase, inPointsLayer, inLocUniqueID, day, start_time, end_time, BufferSize, BufferUnits, DepOrArrChoice, username, password): def runOD(Points, Stops): # Call the OD Cost Matrix service for this set of chunks result = ODservice.GenerateOriginDestinationCostMatrix(Points, Stops, TravelMode, Distance_Units=BufferUnits, Cutoff=BufferSize, Origin_Destination_Line_Shape=PathShape) # Check the status of the result object every 0.5 seconds # until it has a value of 4(succeeded) or greater while result.status < 4: time.sleep(0.5) # Print any warning or error messages returned from the tool result_severity = result.maxSeverity if result_severity == 2: errors = result.getMessages(2) if "No solution found." in errors: # No destinations were found for the origins, which probably just means they were too far away. pass else: arcpy.AddError("An error occured when running the tool") arcpy.AddError(result.getMessages(2)) raise BBB_SharedFunctions.CustomError elif result_severity == 1: arcpy.AddWarning("Warnings were returned when running the tool") arcpy.AddWarning(result.getMessages(1)) # Get the resulting OD Lines and store the stops that are reachable from points. if result_severity != 2: linesSubLayer = result.getOutput(1) with arcpy.da.SearchCursor(linesSubLayer, ["OriginOID", "DestinationOID"]) as ODCursor: for row in ODCursor: UID = pointsOIDdict[row[0]] SID = stopOIDdict[row[1]] PointsAndStops.setdefault(str(UID), []).append(str(SID)) try: # Source FC names are not prepended to field names. arcpy.env.qualifiedFieldNames = False # It's okay to overwrite in-memory stuff. OverwriteOutput = arcpy.env.overwriteOutput # Get the orignal value so we can reset it. arcpy.env.overwriteOutput = True BBB_SharedFunctions.CheckArcVersion(min_version_pro="1.2") BBB_SharedFunctions.ConnectToSQLDatabase(SQLDbase) Specific, day = BBB_SharedFunctions.CheckSpecificDate(day) start_sec, end_sec = BBB_SharedFunctions.ConvertTimeWindowToSeconds(start_time, end_time) # Distance between stops and points BufferSize_padded = BufferSize + (.2 * BufferSize) BufferLinearUnit = str(BufferSize_padded) + " " + BufferUnits # Will we calculate the max wait time? CalcWaitTime = True # Output file designated by user outDir = os.path.dirname(outFile) outFilename = os.path.basename(outFile) ispgdb = "esriDataSourcesGDB.AccessWorkspaceFactory" in arcpy.Describe(outDir).workspaceFactoryProgID inLocUniqueID = BBB_SharedFunctions.HandleOIDUniqueID(inPointsLayer, inLocUniqueID) # ----- Prepare OD service ----- try: arcpy.AddMessage("Obtaining credentials for and information about OD Cost Matrix service...") # Hard-wired OD variables TravelMode = "Walking Distance" PathShape = "None" OD_service_name = "World/OriginDestinationCostMatrix" Utility_service_name = "World/Utilities" # Get the credentials from the signed in user and import the service if username and password: ODservice = BBB_SharedFunctions.import_AGOLservice(OD_service_name, username=username, password=password) Utilityservice = BBB_SharedFunctions.import_AGOLservice(Utility_service_name, username=username, password=password) else: credentials = arcpy.GetSigninToken() if not credentials: arcpy.AddError("Please sign into ArcGIS Online or pass a username and password to the tool.") raise BBB_SharedFunctions.CustomError token = credentials["token"] referer = credentials["referer"] ODservice = BBB_SharedFunctions.import_AGOLservice(OD_service_name, token=token, referer=referer) Utilityservice = BBB_SharedFunctions.import_AGOLservice(Utility_service_name, token=token, referer=referer) # Get the service limits from the OD service (how many origins and destinations allowed) utilresult = Utilityservice.GetToolInfo("asyncODCostMatrix", "GenerateOriginDestinationCostMatrix") utilresultstring = utilresult.getOutput(0) utilresultjson = json.loads(utilresultstring) origin_limit = int(utilresultjson['serviceLimits']['maximumDestinations']) destination_limit = int(utilresultjson['serviceLimits']['maximumOrigins']) except: arcpy.AddError("Failed to obtain credentials for and information about OD Cost Matrix service.") raise # ----- Create a feature class of stops ------ try: arcpy.AddMessage("Getting GTFS stops...") tempstopsname = "Temp_Stops" StopsLayer, StopList = BBB_SharedFunctions.MakeStopsFeatureClass(os.path.join(outDir, tempstopsname)) # Select only the stops within a reasonable distance of points to reduce problem size arcpy.management.MakeFeatureLayer(StopsLayer, "StopsToRemove") arcpy.management.SelectLayerByLocation("StopsToRemove", "WITHIN_A_DISTANCE_GEODESIC", inPointsLayer, BufferLinearUnit, invert_spatial_relationship="INVERT") arcpy.management.DeleteRows("StopsToRemove") arcpy.management.Delete("StopsToRemove") # Make Feature Layer of stops to use later arcpy.management.MakeFeatureLayer(StopsLayer, "StopsLayer") stopsOID = arcpy.Describe("StopsLayer").OIDFieldName except: arcpy.AddError("Error creating feature class of GTFS stops.") raise # ----- Prepare input data ----- try: arcpy.AddMessage("Preparing input points...") # Select only the points within a reasonable distance of stops to reduce problem size temppointsname = outFilename + "_Temp" relevantPoints = os.path.join(outDir, temppointsname) arcpy.management.MakeFeatureLayer(inPointsLayer, "PointsToKeep") arcpy.management.SelectLayerByLocation("PointsToKeep", "WITHIN_A_DISTANCE_GEODESIC", StopsLayer, BufferLinearUnit) num_points = int(arcpy.management.GetCount("PointsToKeep").getOutput(0)) # If the number of points is large, sort them spatially for smart chunking if num_points > origin_limit: shapeFieldName = arcpy.Describe("PointsToKeep").shapeFieldName arcpy.management.Sort("PointsToKeep", relevantPoints, shapeFieldName, "PEANO") # Otherwise, just copy them. else: arcpy.management.CopyFeatures("PointsToKeep", relevantPoints) arcpy.management.Delete("PointsToKeep") # Store OIDs in a dictionary for later joining pointsOIDdict = {} # {OID: inLocUniqueID} with arcpy.da.SearchCursor(relevantPoints, ["OID@", inLocUniqueID]) as cur: for row in cur: pointsOIDdict[row[0]] = row[1] relevantpointsOID = arcpy.Describe(relevantPoints).OIDFieldName except: arcpy.AddError("Error preparing input points for analysis.") raise #----- Create OD Matrix between stops and user's points ----- try: arcpy.AddMessage("Creating OD matrix between points and stops...") arcpy.AddMessage("(This step could take a while for large datasets or buffer sizes.)") global PointsAndStops # PointsAndStops = {LocID: [stop_1, stop_2, ...]} PointsAndStops = {} # Chunk the points to fit the service limits and loop through chunks points_numchunks = int(math.ceil(float(num_points)/origin_limit)) points_chunkstart = 0 points_chunkend = origin_limit current_chunk = 0 for x in range(0, points_numchunks): current_chunk += 1 arcpy.AddMessage("Handling input points chunk %i of %i" % (current_chunk, points_numchunks)) # Select only the points belonging to this chunk points_chunk = sorted(pointsOIDdict.keys())[points_chunkstart:points_chunkend] points_chunkstart = points_chunkend points_chunkend = points_chunkstart + origin_limit if ispgdb: points_selection_query = '[{0}] IN ({1})'.format(relevantpointsOID, ','.join(map(str, points_chunk))) else: points_selection_query = '"{0}" IN ({1})'.format(relevantpointsOID, ','.join(map(str, points_chunk))) arcpy.MakeFeatureLayer_management(relevantPoints, "PointsLayer", points_selection_query) # Select only the stops within the safe buffer of these points arcpy.management.SelectLayerByLocation("StopsLayer", "WITHIN_A_DISTANCE_GEODESIC", "PointsLayer", BufferLinearUnit) num_stops = int(arcpy.GetCount_management("StopsLayer").getOutput(0)) stopOIDdict = {} # {OID: stop_id} with arcpy.da.SearchCursor("StopsLayer", ["OID@", "stop_id"]) as cur: for row in cur: stopOIDdict[row[0]] = row[1] # If the number of stops in range exceeds the destination limit, we have to chunk these as well. if num_stops > destination_limit: stops_numchunks = int(math.ceil(float(num_stops)/destination_limit)) stops_chunkstart = 0 stops_chunkend = destination_limit for x in range(0, stops_numchunks): stops_chunk = sorted(stopOIDdict.keys())[stops_chunkstart:stops_chunkend] stops_chunkstart = stops_chunkend stops_chunkend = stops_chunkstart + destination_limit if ispgdb: stops_selection_query = '[{0}] IN ({1})'.format(stopsOID, ','.join(map(str, stops_chunk))) else: stops_selection_query = '"{0}" IN ({1})'.format(stopsOID, ','.join(map(str, stops_chunk))) arcpy.MakeFeatureLayer_management("StopsLayer", "StopsLayer_Chunk", stops_selection_query) runOD("PointsLayer", "StopsLayer_Chunk") arcpy.management.Delete("StopsLayer_Chunk") # Otherwise, just run them all. else: runOD("PointsLayer", "StopsLayer") # Clean up arcpy.management.Delete("StopsLayer") arcpy.management.Delete("PointsLayer") arcpy.management.Delete(StopsLayer) arcpy.management.Delete(relevantPoints) except: arcpy.AddError("Error creating OD matrix between stops and input points.") raise #----- Query the GTFS data to count the trips at each stop ----- try: arcpy.AddMessage("Calculating the number of transit trips available during the time window...") # Get a dictionary of stop times in our time window {stop_id: [[trip_id, stop_time]]} stoptimedict = BBB_SharedFunctions.CountTripsAtStops(day, start_sec, end_sec, BBB_SharedFunctions.CleanUpDepOrArr(DepOrArrChoice), Specific) except: arcpy.AddError("Error calculating the number of transit trips available during the time window.") raise # ----- Generate output data ----- try: arcpy.AddMessage("Writing output data...") arcpy.management.CopyFeatures(inPointsLayer, outFile) # Add a field to the output file for number of trips and num trips / hour. arcpy.management.AddField(outFile, "NumTrips", "SHORT") arcpy.management.AddField(outFile, "NumTripsPerHr", "DOUBLE") arcpy.management.AddField(outFile, "NumStopsInRange", "SHORT") arcpy.management.AddField(outFile, "MaxWaitTime", "SHORT") with arcpy.da.UpdateCursor(outFile, [inLocUniqueID, "NumTrips", "NumTripsPerHr", "NumStopsInRange", "MaxWaitTime"]) as ucursor: for row in ucursor: try: ImportantStops = PointsAndStops[str(row[0])] except KeyError: # This point had no stops in range ImportantStops = [] NumTrips, NumTripsPerHr, NumStopsInRange, MaxWaitTime =\ BBB_SharedFunctions.RetrieveStatsForSetOfStops( ImportantStops, stoptimedict, CalcWaitTime, start_sec, end_sec) row[1] = NumTrips row[2] = NumTripsPerHr row[3] = NumStopsInRange row[4] = MaxWaitTime ucursor.updateRow(row) except: arcpy.AddError("Error writing output.") raise arcpy.AddMessage("Done!") arcpy.AddMessage("Output files written:") arcpy.AddMessage("- " + outFile) except BBB_SharedFunctions.CustomError: arcpy.AddError("Error counting transit trips at input locations.") pass except: arcpy.AddError("Error counting transit trips at input locations.") raise finally: # Reset overwriteOutput to what it was originally. arcpy.env.overwriteOutput = OverwriteOutput
def runTool(outFile, SQLDbase, inPointsLayer, inLocUniqueID, day, start_time, end_time, BufferSize, BufferUnits, DepOrArrChoice, username, password): def runOD(Points, Stops): # Call the OD Cost Matrix service for this set of chunks result = ODservice.GenerateOriginDestinationCostMatrix( Points, Stops, TravelMode, Distance_Units=BufferUnits, Cutoff=BufferSize, Origin_Destination_Line_Shape=PathShape) # Check the status of the result object every 0.5 seconds # until it has a value of 4(succeeded) or greater while result.status < 4: time.sleep(0.5) # Print any warning or error messages returned from the tool result_severity = result.maxSeverity if result_severity == 2: errors = result.getMessages(2) if "No solution found." in errors: # No destinations were found for the origins, which probably just means they were too far away. pass else: arcpy.AddError("An error occured when running the tool") arcpy.AddError(result.getMessages(2)) raise BBB_SharedFunctions.CustomError elif result_severity == 1: arcpy.AddWarning("Warnings were returned when running the tool") arcpy.AddWarning(result.getMessages(1)) # Get the resulting OD Lines and store the stops that are reachable from points. if result_severity != 2: linesSubLayer = result.getOutput(1) with arcpy.da.SearchCursor( linesSubLayer, ["OriginOID", "DestinationOID"]) as ODCursor: for row in ODCursor: UID = pointsOIDdict[row[0]] SID = stopOIDdict[row[1]] PointsAndStops.setdefault(str(UID), []).append(str(SID)) try: # Source FC names are not prepended to field names. arcpy.env.qualifiedFieldNames = False # It's okay to overwrite in-memory stuff. OverwriteOutput = arcpy.env.overwriteOutput # Get the orignal value so we can reset it. arcpy.env.overwriteOutput = True BBB_SharedFunctions.CheckArcVersion(min_version_pro="1.2") BBB_SharedFunctions.ConnectToSQLDatabase(SQLDbase) Specific, day = BBB_SharedFunctions.CheckSpecificDate(day) start_sec, end_sec = BBB_SharedFunctions.ConvertTimeWindowToSeconds( start_time, end_time) # Distance between stops and points BufferSize_padded = BufferSize + (.2 * BufferSize) BufferLinearUnit = str(BufferSize_padded) + " " + BufferUnits # Will we calculate the max wait time? CalcWaitTime = True # Output file designated by user outDir = os.path.dirname(outFile) outFilename = os.path.basename(outFile) ispgdb = "esriDataSourcesGDB.AccessWorkspaceFactory" in arcpy.Describe( outDir).workspaceFactoryProgID inLocUniqueID = BBB_SharedFunctions.HandleOIDUniqueID( inPointsLayer, inLocUniqueID) # ----- Prepare OD service ----- try: arcpy.AddMessage( "Obtaining credentials for and information about OD Cost Matrix service..." ) # Hard-wired OD variables TravelMode = "Walking Distance" PathShape = "None" OD_service_name = "World/OriginDestinationCostMatrix" Utility_service_name = "World/Utilities" # Get the credentials from the signed in user and import the service if username and password: ODservice = BBB_SharedFunctions.import_AGOLservice( OD_service_name, username=username, password=password) Utilityservice = BBB_SharedFunctions.import_AGOLservice( Utility_service_name, username=username, password=password) else: credentials = arcpy.GetSigninToken() if not credentials: arcpy.AddError( "Please sign into ArcGIS Online or pass a username and password to the tool." ) raise BBB_SharedFunctions.CustomError token = credentials["token"] referer = credentials["referer"] ODservice = BBB_SharedFunctions.import_AGOLservice( OD_service_name, token=token, referer=referer) Utilityservice = BBB_SharedFunctions.import_AGOLservice( Utility_service_name, token=token, referer=referer) # Get the service limits from the OD service (how many origins and destinations allowed) utilresult = Utilityservice.GetToolInfo( "asyncODCostMatrix", "GenerateOriginDestinationCostMatrix") utilresultstring = utilresult.getOutput(0) utilresultjson = json.loads(utilresultstring) origin_limit = int( utilresultjson['serviceLimits']['maximumDestinations']) destination_limit = int( utilresultjson['serviceLimits']['maximumOrigins']) except: arcpy.AddError( "Failed to obtain credentials for and information about OD Cost Matrix service." ) raise # ----- Create a feature class of stops ------ try: arcpy.AddMessage("Getting GTFS stops...") tempstopsname = "Temp_Stops" StopsLayer, StopList = BBB_SharedFunctions.MakeStopsFeatureClass( os.path.join(outDir, tempstopsname)) # Select only the stops within a reasonable distance of points to reduce problem size arcpy.management.MakeFeatureLayer(StopsLayer, "StopsToRemove") arcpy.management.SelectLayerByLocation( "StopsToRemove", "WITHIN_A_DISTANCE_GEODESIC", inPointsLayer, BufferLinearUnit, invert_spatial_relationship="INVERT") arcpy.management.DeleteRows("StopsToRemove") arcpy.management.Delete("StopsToRemove") # Make Feature Layer of stops to use later arcpy.management.MakeFeatureLayer(StopsLayer, "StopsLayer") stopsOID = arcpy.Describe("StopsLayer").OIDFieldName except: arcpy.AddError("Error creating feature class of GTFS stops.") raise # ----- Prepare input data ----- try: arcpy.AddMessage("Preparing input points...") # Select only the points within a reasonable distance of stops to reduce problem size temppointsname = outFilename + "_Temp" relevantPoints = os.path.join(outDir, temppointsname) arcpy.management.MakeFeatureLayer(inPointsLayer, "PointsToKeep") arcpy.management.SelectLayerByLocation( "PointsToKeep", "WITHIN_A_DISTANCE_GEODESIC", StopsLayer, BufferLinearUnit) num_points = int( arcpy.management.GetCount("PointsToKeep").getOutput(0)) # If the number of points is large, sort them spatially for smart chunking if num_points > origin_limit: shapeFieldName = arcpy.Describe("PointsToKeep").shapeFieldName arcpy.management.Sort("PointsToKeep", relevantPoints, shapeFieldName, "PEANO") # Otherwise, just copy them. else: arcpy.management.CopyFeatures("PointsToKeep", relevantPoints) arcpy.management.Delete("PointsToKeep") # Store OIDs in a dictionary for later joining pointsOIDdict = {} # {OID: inLocUniqueID} with arcpy.da.SearchCursor(relevantPoints, ["OID@", inLocUniqueID]) as cur: for row in cur: pointsOIDdict[row[0]] = row[1] relevantpointsOID = arcpy.Describe(relevantPoints).OIDFieldName except: arcpy.AddError("Error preparing input points for analysis.") raise #----- Create OD Matrix between stops and user's points ----- try: arcpy.AddMessage("Creating OD matrix between points and stops...") arcpy.AddMessage( "(This step could take a while for large datasets or buffer sizes.)" ) global PointsAndStops # PointsAndStops = {LocID: [stop_1, stop_2, ...]} PointsAndStops = {} # Chunk the points to fit the service limits and loop through chunks points_numchunks = int(math.ceil(float(num_points) / origin_limit)) points_chunkstart = 0 points_chunkend = origin_limit current_chunk = 0 for x in range(0, points_numchunks): current_chunk += 1 arcpy.AddMessage("Handling input points chunk %i of %i" % (current_chunk, points_numchunks)) # Select only the points belonging to this chunk points_chunk = sorted( pointsOIDdict.keys())[points_chunkstart:points_chunkend] points_chunkstart = points_chunkend points_chunkend = points_chunkstart + origin_limit if ispgdb: points_selection_query = '[{0}] IN ({1})'.format( relevantpointsOID, ','.join(map(str, points_chunk))) else: points_selection_query = '"{0}" IN ({1})'.format( relevantpointsOID, ','.join(map(str, points_chunk))) arcpy.MakeFeatureLayer_management(relevantPoints, "PointsLayer", points_selection_query) # Select only the stops within the safe buffer of these points arcpy.management.SelectLayerByLocation( "StopsLayer", "WITHIN_A_DISTANCE_GEODESIC", "PointsLayer", BufferLinearUnit) num_stops = int( arcpy.GetCount_management("StopsLayer").getOutput(0)) stopOIDdict = {} # {OID: stop_id} with arcpy.da.SearchCursor("StopsLayer", ["OID@", "stop_id"]) as cur: for row in cur: stopOIDdict[row[0]] = row[1] # If the number of stops in range exceeds the destination limit, we have to chunk these as well. if num_stops > destination_limit: stops_numchunks = int( math.ceil(float(num_stops) / destination_limit)) stops_chunkstart = 0 stops_chunkend = destination_limit for x in range(0, stops_numchunks): stops_chunk = sorted(stopOIDdict.keys() )[stops_chunkstart:stops_chunkend] stops_chunkstart = stops_chunkend stops_chunkend = stops_chunkstart + destination_limit if ispgdb: stops_selection_query = '[{0}] IN ({1})'.format( stopsOID, ','.join(map(str, stops_chunk))) else: stops_selection_query = '"{0}" IN ({1})'.format( stopsOID, ','.join(map(str, stops_chunk))) arcpy.MakeFeatureLayer_management( "StopsLayer", "StopsLayer_Chunk", stops_selection_query) runOD("PointsLayer", "StopsLayer_Chunk") arcpy.management.Delete("StopsLayer_Chunk") # Otherwise, just run them all. else: runOD("PointsLayer", "StopsLayer") # Clean up arcpy.management.Delete("StopsLayer") arcpy.management.Delete("PointsLayer") arcpy.management.Delete(StopsLayer) arcpy.management.Delete(relevantPoints) except: arcpy.AddError( "Error creating OD matrix between stops and input points.") raise #----- Query the GTFS data to count the trips at each stop ----- try: arcpy.AddMessage( "Calculating the number of transit trips available during the time window..." ) # Get a dictionary of stop times in our time window {stop_id: [[trip_id, stop_time]]} stoptimedict = BBB_SharedFunctions.CountTripsAtStops( day, start_sec, end_sec, BBB_SharedFunctions.CleanUpDepOrArr(DepOrArrChoice), Specific) except: arcpy.AddError( "Error calculating the number of transit trips available during the time window." ) raise # ----- Generate output data ----- try: arcpy.AddMessage("Writing output data...") arcpy.management.CopyFeatures(inPointsLayer, outFile) # Add a field to the output file for number of trips and num trips / hour. arcpy.management.AddField(outFile, "NumTrips", "SHORT") arcpy.management.AddField(outFile, "NumTripsPerHr", "DOUBLE") arcpy.management.AddField(outFile, "NumStopsInRange", "SHORT") arcpy.management.AddField(outFile, "MaxWaitTime", "SHORT") with arcpy.da.UpdateCursor(outFile, [ inLocUniqueID, "NumTrips", "NumTripsPerHr", "NumStopsInRange", "MaxWaitTime" ]) as ucursor: for row in ucursor: try: ImportantStops = PointsAndStops[str(row[0])] except KeyError: # This point had no stops in range ImportantStops = [] NumTrips, NumTripsPerHr, NumStopsInRange, MaxWaitTime =\ BBB_SharedFunctions.RetrieveStatsForSetOfStops( ImportantStops, stoptimedict, CalcWaitTime, start_sec, end_sec) row[1] = NumTrips row[2] = NumTripsPerHr row[3] = NumStopsInRange row[4] = MaxWaitTime ucursor.updateRow(row) except: arcpy.AddError("Error writing output.") raise arcpy.AddMessage("Done!") arcpy.AddMessage("Output files written:") arcpy.AddMessage("- " + outFile) except BBB_SharedFunctions.CustomError: arcpy.AddError("Error counting transit trips at input locations.") pass except: arcpy.AddError("Error counting transit trips at input locations.") raise finally: # Reset overwriteOutput to what it was originally. arcpy.env.overwriteOutput = OverwriteOutput