def runTool(outDir, outGDB, inSQLDbase, inNetworkDataset, imp, BufferSize, restrictions, TrimSettings): try: # ----- Set up the run ----- try: BBB_SharedFunctions.CheckArcVersion(min_version_pro="1.2") BBB_SharedFunctions.CheckArcInfoLicense() BBB_SharedFunctions.CheckOutNALicense() BBB_SharedFunctions.CheckWorkspace() # 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 # Append .gdb to geodatabase name. if not outGDB.lower().endswith(".gdb"): outGDB += ".gdb" outGDBwPath = os.path.join(outDir, outGDB) # Create a file geodatabase for the results. arcpy.management.CreateFileGDB(outDir, outGDB) # Make a copy of the input SQL file in the Step 1 output so we can modify it. SQLDbase = os.path.join(outGDBwPath, "Step1_GTFS.sql") copyfile(inSQLDbase, SQLDbase) # Connect to or create the SQL file. conn = sqlite3.connect(SQLDbase) c = BBB_SharedFunctions.c = conn.cursor() impedanceAttribute = BBB_SharedFunctions.CleanUpImpedance(imp) TrimPolys, TrimPolysValue = BBB_SharedFunctions.CleanUpTrimSettings( TrimSettings) except: arcpy.AddError("Error setting up run.") raise #----- Make a feature class of GTFS stops that we can use for buffers ----- try: # Create a feature class of transit stops arcpy.AddMessage("Creating a feature class of GTFS stops...") StopsLayer, StopIDList = BBB_SharedFunctions.MakeStopsFeatureClass( os.path.join(outGDBwPath, "Step1_Stops")) except: arcpy.AddError("Error creating a feature class of GTFS stops.") raise #----- Create Service Areas around all stops in the system ----- try: arcpy.AddMessage("Creating service areas around stops...") arcpy.AddMessage( "(This step will take a while for large networks.)") polygons = BBB_SharedFunctions.MakeServiceAreasAroundStops( StopsLayer, inNetworkDataset, impedanceAttribute, BufferSize, restrictions, TrimPolys, TrimPolysValue) except: arcpy.AddError("Error creating service areas around stops.") raise #----- Post-process the polygons to prepare for Step 2 ----- try: arcpy.AddMessage("Reformatting polygons for further analysis...") arcpy.AddMessage( "(This step will take a while for large networks.)") # ----- Flatten the overlapping service area polygons ----- # Use World Cylindrical Equal Area (WKID 54034) to ensure proper use of cluster tolerance in meters arcpy.env.outputCoordinateSystem = BBB_SharedFunctions.WorldCylindrical # Flatten the overlapping polygons. This will ultimately be our output. # Dummy points to use in FeatureToPolygon to get rid of unnecessary fields. dummypoints = arcpy.management.CreateFeatureclass( "in_memory", "DummyPoints", "POINT") # The flattened polygons will be our ultimate output in the end (final # output of step 2). FlatPolys = os.path.join(outGDBwPath, "Step1_FlatPolys") # FeatureToPolygon flattens overalpping polys. # Set a large cluster tolerance to eliminate small sliver polygons and to # keep the output file size down. Boundaries may move up to the distance # specified in the cluster tolerance, but some amount of movement is # acceptable, as service area polygons are inexact anyway. # The large cluster tolerance may cause some geometry issues with the output # later, but this is the best solution I've found so far that doesn't eat # up too much analysis time and memory clusTol = "5 meters" arcpy.management.FeatureToPolygon(polygons, FlatPolys, clusTol, "", dummypoints) arcpy.management.Delete(dummypoints) # Add a field to the output file for number of trips and num trips / hour. # Also create a polygon id field so we can keep track of them. arcpy.management.AddField(FlatPolys, "PolyID", "LONG") arcpy.management.AddField(FlatPolys, "NumTrips", "LONG") arcpy.management.AddField(FlatPolys, "NumTripsPerHr", "DOUBLE") arcpy.management.AddField(FlatPolys, "NumStopsInRange", "LONG") arcpy.management.AddField(FlatPolys, "MaxWaitTime", "DOUBLE") # ----- Create stacked points, one for each original SA polygon ----- # Create points for use in the Identity tool (one point per poly) FlattenedPoints = os.path.join(outGDBwPath, "Step1_FlattenedPoints") arcpy.management.FeatureToPoint(FlatPolys, FlattenedPoints, "INSIDE") # Use Identity to stack points and keep the stop_ids from the original SAs. # Results in a points layer with fields ORIG_FID for the IDs of the # flattened polygons and a stop_id column with the stop ids. # Points are stacked, and each has only one stop_id. StackedPoints = os.path.join(outGDBwPath, "Step1_StackedPoints") arcpy.analysis.Identity(FlattenedPoints, polygons, StackedPoints) arcpy.management.Delete(FlattenedPoints) # ----- Read the Stacked Points into an SQL table ----- # Create a SQL table associating the Polygon FID with the stop_ids that serve it. c.execute("DROP TABLE IF EXISTS StackedPoints;") schema = "Polygon_FID LONG, stop_id TEXT" create_stmt = "CREATE TABLE StackedPoints (%s);" % schema c.execute(create_stmt) # Add data to the table. Track Polygon IDs with no associated stop_ids so we can delete them. FIDsToDelete = [] AddToStackedPts = [] with arcpy.da.SearchCursor( StackedPoints, ["ORIG_FID", "stop_id"]) as StackedPtCursor: for row in StackedPtCursor: if not row[1]: FIDsToDelete.append(row[0]) else: AddToStackedPts.append(( row[0], row[1], )) # Add the OD items to the SQL table c.executemany( '''INSERT INTO StackedPoints \ (Polygon_FID, stop_id) \ VALUES (?, ?);''', AddToStackedPts) conn.commit() arcpy.management.Delete(StackedPoints) FIDsToDelete = set(FIDsToDelete) # ----- Delete polygons not associated with any stop_ids ----- # These were generated by the FeatureToPolygon tool in areas completely # surrounded by other polygons and aren't associated with any stops. # Make feature layer containing only the polygons we want to delete. desc2 = arcpy.Describe(FlatPolys) OutputOIDName = desc2.OIDFieldName # Anything with 0 area will just cause problems later. WhereClause = '"Shape_Area" = 0' if FIDsToDelete: WhereClause += ' OR "' + OutputOIDName + '" IN (' for FID in FIDsToDelete: WhereClause += str(FID) + ", " WhereClause = WhereClause[:-2] + ")" arcpy.management.MakeFeatureLayer(FlatPolys, "FlatPolysLayer", WhereClause) # Delete the polygons that don't correspond to any stop_ids. arcpy.management.DeleteFeatures("FlatPolysLayer") # ----- Populate the PolyID field ----- # Set PolyID equal to the OID. expression = "!" + OutputOIDName + "!" arcpy.management.CalculateField(FlatPolys, "PolyID", expression, "PYTHON") except: arcpy.AddError("Error post-processing polygons") raise arcpy.AddMessage("Done!") arcpy.AddMessage("Files written to output geodatabase " + outGDBwPath + ":") arcpy.AddMessage("- Step1_Stops") arcpy.AddMessage("- Step1_FlatPolys") arcpy.AddMessage("- Step1_GTFS.sql") # Tell the tool that this is output. This will add the output to the map. arcpy.SetParameterAsText(8, os.path.join(outGDBwPath, "Step1_Stops")) arcpy.SetParameterAsText(9, os.path.join(outGDBwPath, "Step1_FlatPolys")) arcpy.SetParameterAsText(10, os.path.join(outGDBwPath, "Step1_GTFS.sql")) except BBB_SharedFunctions.CustomError: arcpy.AddError("Failed to create BetterBusBuffers polygons.") pass except: arcpy.AddError("Failed to create BetterBusBuffers polygons.") raise
def runTool(outGDB, SQLDbase, RouteText, inNetworkDataset, imp, BufferSize, restrictions, TrimSettings): try: OverwriteOutput = arcpy.env.overwriteOutput # Get the orignal value so we can reset it. arcpy.env.overwriteOutput = True # Source FC names are not prepended to field names. arcpy.env.qualifiedFieldNames = False BBB_SharedFunctions.CheckArcVersion(min_version_pro="1.2") BBB_SharedFunctions.CheckOutNALicense() BBB_SharedFunctions.CheckWorkspace() # ===== Get trips and stops associated with this route ===== # ----- Figure out which route the user wants to analyze based on the text input ----- try: arcpy.AddMessage("Gathering route, trip, and stop information...") # Connect to or create the SQL file. conn = sqlite3.connect(SQLDbase) c = BBB_SharedFunctions.c = conn.cursor() # Get list of routes in the GTFS data routefetch = "SELECT route_short_name, route_long_name, route_id FROM routes;" c.execute(routefetch) # Extract the route_id based on what the user picked from the GUI list # It's better to do it by searching the database instead of trying to extract # the route_id from the text they chose because we don't know what kind of # characters will be in the route names and id, so parsing could be unreliable route_id = "" for route in c: routecheck = route[0] + ": " + route[1] + " [" + route[2] + "]" if routecheck == RouteText: route_id = route[2] route_short_name = route[0] break if not route_id: arcpy.AddError("Could not parse route selection.") raise BBB_SharedFunctions.CustomError # Name feature classes outStopsname = arcpy.ValidateTableName("Stops_" + route_short_name, outGDB) outPolysname = arcpy.ValidateTableName( "Buffers_" + route_short_name, outGDB) except: arcpy.AddError("Error determining route_id for analysis.") raise # ----- Get trips associated with route and split into directions ----- try: # Some GTFS datasets use the same route_id to identify trips traveling in # either direction along a route. Others identify it as a different route. # We will consider each direction separately if there is more than one. # Get list of trips trip_route_dict = {} triproutefetch = ''' SELECT trip_id, direction_id FROM trips WHERE route_id='%s' ;''' % route_id c.execute(triproutefetch) # Fill some dictionaries for use later. trip_dir_dict = {} # {Direction: [trip_id, trip_id, ...]} for triproute in c: trip_dir_dict.setdefault(triproute[1], []).append(triproute[0]) if not trip_dir_dict: arcpy.AddError( "There are no trips in the GTFS data for the route \ you have selected (%s). Please select a different route or fix your GTFS \ dataset." % RouteText) raise BBB_SharedFunctions.CustomError except: arcpy.AddError("Error getting trips associated with route.") raise # ----- Get list of stops associated with trips and split into directions ----- try: # If a stop is used for trips going in both directions, count them separately. # Select unique set of stops used by trips in each direction stoplist = {} # {Direction: [stop_id, stop_id, ...]} for direction in trip_dir_dict: stops = [] for trip in trip_dir_dict[direction]: stopsfetch = '''SELECT stop_id FROM stop_times WHERE trip_id == ?''' c.execute(stopsfetch, (trip, )) for stop in c: stops.append(stop[0]) stoplist[direction] = list(set(stops)) # If there is more than one direction, we will append the direction number # to the output fc names, so add an _ here for prettiness. if len(stoplist) > 1: arcpy.AddMessage( "Route %s contains trips going in more than one \ direction. A separate feature class will be created for each direction, and the \ GTFS direction_id will be appended to the feature class name." % route_short_name) outStopsname += "_" outPolysname += "_" except: arcpy.AddError("Error getting stops associated with route.") raise # ===== Create output ===== # ----- Create a feature class of stops ------ try: arcpy.AddMessage("Creating feature class of GTFS stops...") for direction in stoplist: stops = stoplist[direction] outputname = outStopsname if direction != None: outputname += str(direction) outStops = os.path.join(outGDB, outputname) outStops, outStopList = BBB_SharedFunctions.MakeStopsFeatureClass( outStops, stops) # Add a route_id and direction_id field and populate it arcpy.management.AddField(outStops, "route_id", "TEXT") arcpy.management.AddField(outStops, "direction_id", "TEXT") fields = ["route_id", "direction_id"] with arcpy.da.UpdateCursor(outStops, fields) as cursor: for row in cursor: row[0] = route_id row[1] = direction cursor.updateRow(row) except: arcpy.AddError("Error creating feature class of GTFS stops.") raise #----- Create Service Areas around stops ----- try: arcpy.AddMessage("Creating buffers around stops...") for direction in stoplist: outputname = outStopsname if direction != None: outputname += str(direction) outStops = os.path.join(outGDB, outputname) TrimPolys, TrimPolysValue = BBB_SharedFunctions.CleanUpTrimSettings( TrimSettings) polygons = BBB_SharedFunctions.MakeServiceAreasAroundStops( outStops, inNetworkDataset, BBB_SharedFunctions.CleanUpImpedance(imp), BufferSize, restrictions, TrimPolys, TrimPolysValue) # Join stop information to polygons and save as feature class arcpy.management.AddJoin(polygons, "stop_id", outStops, "stop_id") outPolys = outPolysname if direction != None: outPolys += str(direction) outPolysFC = os.path.join(outGDB, outPolys) arcpy.management.CopyFeatures(polygons, outPolysFC) # Add a route_id and direction_id field and populate it arcpy.management.AddField(outPolysFC, "route_id", "TEXT") arcpy.management.AddField(outPolysFC, "direction_id", "TEXT") fields = ["route_id", "direction_id"] with arcpy.da.UpdateCursor(outPolysFC, fields) as cursor: for row in cursor: row[0] = route_id row[1] = direction cursor.updateRow(row) except: arcpy.AddError("Error creating buffers around stops.") raise arcpy.AddMessage("Done!") arcpy.AddMessage("Output written to %s is:" % outGDB) outFClist = [] for direction in stoplist: outPolysFC = outPolysname outStopsFC = outStopsname if direction != None: outStopsFC += str(direction) outPolysFC += str(direction) outFClist.append(outStopsFC) outFClist.append(outPolysFC) arcpy.AddMessage("- " + outStopsFC) arcpy.AddMessage("- " + outPolysFC) # Tell the tool that this is output. This will add the output to the map. outFClistwpaths = [os.path.join(outGDB, fc) for fc in outFClist] arcpy.SetParameterAsText(8, ';'.join(outFClistwpaths)) except BBB_SharedFunctions.CustomError: arcpy.AddError("Failed to create buffers around stops for this route.") pass except: arcpy.AddError("Failed to create buffers around stops for this route.") raise finally: if OverwriteOutput: arcpy.env.overwriteOutput = OverwriteOutput
def runTool(outFile, SQLDbase, inPointsLayer, inLocUniqueID, day, start_time, end_time, inNetworkDataset, imp, BufferSize, restrictions, DepOrArrChoice): 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") ProductName = BBB_SharedFunctions.ProductName BBB_SharedFunctions.CheckWorkspace() BBB_SharedFunctions.CheckOutNALicense() BBB_SharedFunctions.ConnectToSQLDatabase(SQLDbase) Specific, day = BBB_SharedFunctions.CheckSpecificDate(day) start_sec, end_sec = BBB_SharedFunctions.ConvertTimeWindowToSeconds( start_time, end_time) # Will we calculate the max wait time? CalcWaitTime = True impedanceAttribute = BBB_SharedFunctions.CleanUpImpedance(imp) # Hard-wired OD variables ExcludeRestricted = "EXCLUDE" PathShape = "NO_LINES" accumulate = "" uturns = "ALLOW_UTURNS" hierarchy = "NO_HIERARCHY" # Output file designated by user outDir = os.path.dirname(outFile) outFilename = os.path.basename(outFile) inLocUniqueID = BBB_SharedFunctions.HandleOIDUniqueID( inPointsLayer, inLocUniqueID) inLocUniqueID_qualified = inLocUniqueID + "_Input" arcpy.AddMessage("Run set up successfully.") # ----- Create a feature class of stops ------ try: arcpy.AddMessage("Getting GTFS stops...") tempstopsname = "Temp_Stops" if ".shp" in outFilename: tempstopsname += ".shp" StopsLayer, StopList = BBB_SharedFunctions.MakeStopsFeatureClass( os.path.join(outDir, tempstopsname)) except: arcpy.AddError("Error creating feature class of GTFS stops.") 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.)" ) # Name to refer to OD matrix layer outNALayer_OD = "ODMatrix" # ODLayer is the NA Layer object returned by getOutput(0) ODLayer = arcpy.na.MakeODCostMatrixLayer( inNetworkDataset, outNALayer_OD, impedanceAttribute, BufferSize, "", accumulate, uturns, restrictions, hierarchy, "", PathShape).getOutput(0) # To refer to the OD sublayers, get the sublayer names. This is essential for localization. naSubLayerNames = arcpy.na.GetNAClassNames(ODLayer) points = naSubLayerNames["Origins"] stops = naSubLayerNames["Destinations"] # Add a field for stop_id as a unique identifier for stops. arcpy.na.AddFieldToAnalysisLayer(outNALayer_OD, stops, "stop_id", "TEXT") # Specify the field mappings for the stop_id field. fieldMappingStops = arcpy.na.NAClassFieldMappings(ODLayer, stops) fieldMappingStops["Name"].mappedFieldName = "stop_id" fieldMappingStops["stop_id"].mappedFieldName = "stop_id" # Add the GTFS stops as locations for the analysis. arcpy.na.AddLocations(outNALayer_OD, stops, StopsLayer, fieldMappingStops, "500 meters", "", "", "", "", "", "", ExcludeRestricted) # Clear out the memory because we don't need this anymore. arcpy.management.Delete(StopsLayer) # Add a field for unique identifier for points. arcpy.na.AddFieldToAnalysisLayer(outNALayer_OD, points, inLocUniqueID_qualified, "TEXT") # Specify the field mappings for the unique id field. fieldMappingPoints = arcpy.na.NAClassFieldMappings(ODLayer, points) fieldMappingPoints["Name"].mappedFieldName = inLocUniqueID fieldMappingPoints[ inLocUniqueID_qualified].mappedFieldName = inLocUniqueID # Add the input points as locations for the analysis. arcpy.na.AddLocations(outNALayer_OD, points, inPointsLayer, fieldMappingPoints, "500 meters", "", "", "", "", "", "", ExcludeRestricted) # Solve the OD matrix. try: arcpy.na.Solve(outNALayer_OD) except: errs = arcpy.GetMessages(2) if "No solution found" in errs: impunits = imp.split(" (Units: ")[1].split(")")[0] arcpy.AddError( "No transit stops were found within a %s %s walk of any of your input points. \ Consequently, there is no transit service available to your input points, so no output will be generated." % (str(BufferSize), impunits)) else: arcpy.AddError( "Failed to calculate travel time or distance between transit stops and input points. OD Cost Matrix error messages:" ) arcpy.AddError(errs) raise BBB_SharedFunctions.CustomError # Make layer objects for each sublayer we care about. if ProductName == 'ArcGISPro': naSubLayerNames = arcpy.na.GetNAClassNames(ODLayer) subLayerDict = dict( (lyr.name, lyr) for lyr in ODLayer.listLayers()) subLayers = {} for subL in naSubLayerNames: subLayers[subL] = subLayerDict[naSubLayerNames[subL]] else: subLayers = dict( (lyr.datasetName, lyr) for lyr in arcpy.mapping.ListLayers(ODLayer)[1:]) linesSubLayer = subLayers["ODLines"] pointsSubLayer = subLayers["Origins"] stopsSubLayer = subLayers["Destinations"] # Get the OID fields, just to be thorough desc1 = arcpy.Describe(pointsSubLayer) points_OID = desc1.OIDFieldName desc2 = arcpy.Describe(stopsSubLayer) stops_OID = desc2.OIDFieldName # Join polygons layer with input facilities to port over the stop_id arcpy.management.JoinField(linesSubLayer, "OriginID", pointsSubLayer, points_OID, [inLocUniqueID_qualified]) arcpy.management.JoinField(linesSubLayer, "DestinationID", stopsSubLayer, stops_OID, ["stop_id"]) # Use searchcursor on lines to find the stops that are reachable from points. global PointsAndStops # PointsAndStops = {LocID: [stop_1, stop_2, ...]} PointsAndStops = {} ODCursor = arcpy.da.SearchCursor( linesSubLayer, [inLocUniqueID_qualified, "stop_id"]) for row in ODCursor: PointsAndStops.setdefault(str(row[0]), []).append(str(row[1])) del ODCursor 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. if ".shp" in outFilename: arcpy.management.AddField(outFile, "NumTrips", "SHORT") arcpy.management.AddField(outFile, "TripsPerHr", "DOUBLE") arcpy.management.AddField(outFile, "NumStops", "SHORT") arcpy.management.AddField(outFile, "MaxWaitTm", "SHORT") else: arcpy.management.AddField(outFile, "NumTrips", "SHORT") arcpy.management.AddField(outFile, "NumTripsPerHr", "DOUBLE") arcpy.management.AddField(outFile, "NumStopsInRange", "SHORT") arcpy.management.AddField(outFile, "MaxWaitTime", "SHORT") if ".shp" in outFilename: ucursor = arcpy.da.UpdateCursor(outFile, [ inLocUniqueID[0:10], "NumTrips", "TripsPerHr", "NumStops", "MaxWaitTm" ]) else: ucursor = arcpy.da.UpdateCursor(outFile, [ inLocUniqueID, "NumTrips", "NumTripsPerHr", "NumStopsInRange", "MaxWaitTime" ]) 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 if ".shp" in outFilename and MaxWaitTime == None: row[4] = -1 else: 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