class CrossKFunction(object): ### # Initialize the tool. ### def __init__(self): self.label = "Cross K Function" self.description = "Uses a Cross K Function to analyze clustering and dispersion trends in a set of origin and destination points (for example, bridges and crashes)." self.canRunInBackground = False env.overwriteOutput = True self.kfHelper = KFunctionHelper() ### # Get input from the users. ### def getParameterInfo(self): # Input origin points features. srcPoints = arcpy.Parameter( displayName="Input Origin Points Feature Dataset (e.g. bridges)", name="srcPoints", datatype="Feature Class", parameterType="Required", direction="Input") srcPoints.filter.list = ["Point"] # Input destination origin features. destPoints = arcpy.Parameter( displayName= "Input Destination Points Feature Dataset (e.g. crashes)", name="destPoints", datatype="Feature Class", parameterType="Required", direction="Input") destPoints.filter.list = ["Point"] # Network dataset. networkDataset = arcpy.Parameter(displayName="Input Network Dataset", name="network_dataset", datatype="Network Dataset Layer", parameterType="Required", direction="Input") # Number of distance increments. numBands = arcpy.Parameter( displayName="Input Number of Distance Bands", name="num_dist_bands", datatype="Long", parameterType="Optional", direction="Input") # Beginning distance. begDist = arcpy.Parameter(displayName="Input Beginning Distance", name="beginning_distance", datatype="Double", parameterType="Required", direction="Input") begDist.value = 0 # Distance increment. distInc = arcpy.Parameter(displayName="Input Distance Increment", name="distance_increment", datatype="Double", parameterType="Required", direction="Input") distInc.value = 1000 # Snap distance. snapDist = arcpy.Parameter(displayName="Input Snap Distance", name="snap_distance", datatype="Double", parameterType="Required", direction="Input") snapDist.value = 25 # Output location. outNetKLoc = arcpy.Parameter( displayName="Output Location (Database Path)", name="out_location", datatype="DEWorkspace", parameterType="Required", direction="Input") outNetKLoc.value = arcpy.env.workspace # The raw ODCM data. outRawODCMFCName = arcpy.Parameter( displayName="Raw ODCM Data Table", name="output_raw_odcm_feature_class", datatype="GPString", parameterType="Required", direction="Output") outRawODCMFCName.value = "Cross_K_Raw_ODCM_Data" # The raw data feature class (e.g. observed and random point computations). outRawFCName = arcpy.Parameter( displayName="Raw Network-K Data Table (Raw Analysis Data)", name="output_raw_analysis_feature_class", datatype="GPString", parameterType="Required", direction="Output") outRawFCName.value = "Cross_K_Raw_Analysis_Data" # The analysis feature class. outAnlFCName = arcpy.Parameter( displayName="Network-K Summary Data (Plottable Data)", name="output_analysis_feature_class", datatype="GPString", parameterType="Required", direction="Output") outAnlFCName.value = "Cross_K_Summary_Data" # Confidence envelope (number of permutations). numPerms = arcpy.Parameter( displayName="Number of Random Point Permutations", name="num_permutations", datatype="GPString", parameterType="Required", direction="Input") permKeys = self.kfHelper.getPermutationSelection().keys() numPerms.filter.list = permKeys numPerms.value = permKeys[0] # Projected coordinate system. outCoordSys = arcpy.Parameter( displayName= "Output Network Dataset Length Projected Coordinate System", name="coordinate_system", datatype="GPSpatialReference", parameterType="Optional", direction="Input") # Number of points field. numPointsFieldName = arcpy.Parameter( displayName="Number of Points Field", name="num_points_field", datatype="GPString", parameterType="Optional", direction="Input") return [ srcPoints, destPoints, networkDataset, numBands, begDist, distInc, snapDist, outNetKLoc, outRawODCMFCName, outRawFCName, outAnlFCName, numPerms, outCoordSys, numPointsFieldName ] ### # Check if the tool is available for use. ### def isLicensed(self): # Network Analyst tools must be available. return arcpy.CheckExtension("Network") == "Available" ### # Set parameter defaults. ### def updateParameters(self, parameters): networkDataset = parameters[2].value outCoordSys = parameters[12].value # Default the coordinate system. if networkDataset is not None and outCoordSys is None: ndDesc = arcpy.Describe(networkDataset) # If the network dataset's coordinate system is a projected one, # use its coordinate system as the defualt. if (ndDesc.spatialReference.projectionName != "" and ndDesc.spatialReference.linearUnitName == "Meter" and ndDesc.spatialReference.factoryCode != 0): parameters[12].value = ndDesc.spatialReference.factoryCode # Set the source of the fields (the network dataset). if networkDataset is not None: parameters[13].filter.list = self.kfHelper.getEdgeSourceFieldNames( networkDataset) ### # If any fields are invalid, show an appropriate error message. ### def updateMessages(self, parameters): outCoordSys = parameters[12].value if outCoordSys is not None: if outCoordSys.projectionName == "": parameters[12].setErrorMessage( "Output coordinate system must be a projected coordinate system." ) elif outCoordSys.linearUnitName != "Meter": parameters[12].setErrorMessage( "Output coordinate system must have a linear unit code of 'Meter.'" ) else: parameters[12].clearMessage() ### # Execute the tool. ### def execute(self, parameters, messages): srcPoints = parameters[0].valueAsText destPoints = parameters[1].valueAsText networkDataset = parameters[2].valueAsText numBands = parameters[3].value begDist = parameters[4].value distInc = parameters[5].value snapDist = parameters[6].value outNetKLoc = parameters[7].valueAsText outRawODCMFCName = parameters[8].valueAsText outRawFCName = parameters[9].valueAsText outAnlFCName = parameters[10].valueAsText numPerms = self.kfHelper.getPermutationSelection()[ parameters[11].valueAsText] outCoordSys = parameters[12].value numPointsFieldName = parameters[13].value ndDesc = arcpy.Describe(networkDataset) gkfSvc = GlobalKFunctionSvc() # Refer to the note in the NetworkDatasetLength tool. if outCoordSys is None: outCoordSys = ndDesc.spatialReference messages.addMessage("\nOrigin points: {0}".format(srcPoints)) messages.addMessage("Destination points: {0}".format(destPoints)) messages.addMessage("Network dataset: {0}".format(networkDataset)) messages.addMessage("Number of distance bands: {0}".format(numBands)) messages.addMessage("Beginning distance: {0}".format(begDist)) messages.addMessage("Distance increment: {0}".format(distInc)) messages.addMessage("Snap distance: {0}".format(snapDist)) messages.addMessage( "Path to output cross-K feature class: {0}".format(outNetKLoc)) messages.addMessage( "Raw ODCM data table: {0}".format(outRawODCMFCName)) messages.addMessage( "Raw cross-K data table (raw analysis data): {0}".format( outRawFCName)) messages.addMessage( "Cross-K summary data (plottable data): {0}".format(outAnlFCName)) messages.addMessage( "Number of random permutations: {0}".format(numPerms)) messages.addMessage( "Network dataset length projected coordinate system: {0}".format( outCoordSys.name)) messages.addMessage( "Number of Points Field Name: {0}\n".format(numPointsFieldName)) # Calculate the length of the network. networkLength = self.kfHelper.calculateLength(networkDataset, outCoordSys) messages.addMessage("Total network length: {0}".format(networkLength)) # Count the number of crashes. numDests = self.kfHelper.countNumberOfFeatures( os.path.join(outNetKLoc, destPoints)) # Set up a cutoff lenght for the ODCM data if possible. (Optimization.) cutoff = gkfSvc.getCutoff(numBands, distInc, begDist) # The results of all the calculations end up here. netKCalculations = [] # Use a mutable container for the number of bands so that the below callback # can write to it. The "nonlocal" keyword not available in Python 2.x. numBandsCont = [numBands] # Callback function that does the Network K calculation on an OD cost matrix. def doNetKCalc(odDists, iteration): # Do the actual network k-function calculation. netKCalc = CrossKCalculation(networkLength, numDests, odDists, begDist, distInc, numBandsCont[0]) netKCalculations.append(netKCalc.getDistanceBands()) # If the user did not specifiy a number of distance bands explicitly, # store the number of bands. It's computed from the observed data. if numBandsCont[0] is None: numBandsCont[0] = netKCalc.getNumberOfDistanceBands() # Generate the ODCM permutations, including the ODCM for the observed data. # doNetKCalc is called on each iteration. randODCMPermSvc = RandomODCMPermutationsSvc() randODCMPermSvc.generateODCMPermutations( "Cross Analysis", srcPoints, destPoints, networkDataset, snapDist, cutoff, outNetKLoc, outRawODCMFCName, numPerms, outCoordSys, numPointsFieldName, messages, doNetKCalc) # Store the raw analysis data. messages.addMessage("Writing raw analysis data.") gkfSvc.writeRawAnalysisData(outNetKLoc, outRawFCName, netKCalculations) # Analyze the data and store the results. messages.addMessage("Analyzing data.") gkfSvc.writeAnalysisSummaryData(numPerms, netKCalculations, outNetKLoc, outAnlFCName)
class GlobalKFunction(object): ### # Initialize the tool. ### def __init__(self): self.label = "Global K Function" self.description = "Uses a Global Network K Function to analyze clustering and dispersion trends in a set of crash points." self.canRunInBackground = False env.overwriteOutput = True self.kfHelper = KFunctionHelper() ### # Get input from the users. ### def getParameterInfo(self): # Input origin features. points = arcpy.Parameter( displayName="Input Points Feature Dataset", name="points", datatype="Feature Class", parameterType="Required", direction="Input") points.filter.list = ["Point"] # Network dataset. networkDataset = arcpy.Parameter( displayName="Input Network Dataset", name = "network_dataset", datatype="Network Dataset Layer", parameterType="Required", direction="Input") # Number of distance increments. numBands = arcpy.Parameter( displayName="Input Number of Distance Bands", name="num_dist_bands", datatype="Long", parameterType="Optional", direction="Input") # Beginning distance. begDist = arcpy.Parameter( displayName="Input Beginning Distance", name="beginning_distance", datatype="Double", parameterType="Required", direction="Input") begDist.value = 0 # Distance increment. distInc = arcpy.Parameter( displayName="Input Distance Increment", name="distance_increment", datatype="Double", parameterType="Required", direction="Input") distInc.value = 1000 # Snap distance. snapDist = arcpy.Parameter( displayName="Input Snap Distance", name="snap_distance", datatype="Double", parameterType="Required", direction="Input") snapDist.value = 25 # Output location. outNetKLoc = arcpy.Parameter( displayName="Output Location (Database Path)", name="out_location", datatype="DEWorkspace", parameterType="Required", direction="Input") outNetKLoc.value = arcpy.env.workspace # The raw ODCM data. outRawODCMFCName = arcpy.Parameter( displayName="Raw ODCM Data Table", name = "output_raw_odcm_feature_class", datatype="GPString", parameterType="Required", direction="Output") outRawODCMFCName.value = "Global_K_Raw_ODCM_Data" # The raw data feature class (e.g. observed and random point computations). outRawFCName = arcpy.Parameter( displayName="Raw Global-K Data Table (Raw Analysis Data)", name = "output_raw_analysis_feature_class", datatype="GPString", parameterType="Required", direction="Output") outRawFCName.value = "Global_K_Raw_Analysis_Data" # The analysis feature class. outAnlFCName = arcpy.Parameter( displayName="Global-K Summary Data (Plottable Data)", name = "output_analysis_feature_class", datatype="GPString", parameterType="Required", direction="Output") outAnlFCName.value = "Global_K_Summary_Data" # Confidence envelope (number of permutations). numPerms = arcpy.Parameter( displayName="Number of Random Point Permutations", name = "num_permutations", datatype="GPString", parameterType="Required", direction="Input") permKeys = self.kfHelper.getPermutationSelection().keys() numPerms.filter.list = permKeys numPerms.value = permKeys[0] # Projected coordinate system. outCoordSys = arcpy.Parameter( displayName="Output Network Dataset Length Projected Coordinate System", name="coordinate_system", datatype="GPSpatialReference", parameterType="Optional", direction="Input") # Number of points field. numPointsFieldName = arcpy.Parameter( displayName="Number of Points Field", name = "num_points_field", datatype="GPString", parameterType="Optional", direction="Input") return [points, networkDataset, numBands, begDist, distInc, snapDist, outNetKLoc, outRawODCMFCName, outRawFCName, outAnlFCName, numPerms, outCoordSys, numPointsFieldName] ### # Check if the tool is available for use. ### def isLicensed(self): # Network Analyst tools must be available. return arcpy.CheckExtension("Network") == "Available" ### # Set parameter defaults. ### def updateParameters(self, parameters): networkDataset = parameters[1].value outCoordSys = parameters[11].value # Default the coordinate system. if networkDataset is not None and outCoordSys is None: ndDesc = arcpy.Describe(networkDataset) # If the network dataset's coordinate system is a projected one, # use its coordinate system as the defualt. if (ndDesc.spatialReference.projectionName != "" and ndDesc.spatialReference.linearUnitName == "Meter" and ndDesc.spatialReference.factoryCode != 0): parameters[11].value = ndDesc.spatialReference.factoryCode # Set the source of the fields (the network dataset). if networkDataset is not None: parameters[12].filter.list = self.kfHelper.getEdgeSourceFieldNames(networkDataset) ### # If any fields are invalid, show an appropriate error message. ### def updateMessages(self, parameters): outCoordSys = parameters[11].value if outCoordSys is not None: if outCoordSys.projectionName == "": parameters[11].setErrorMessage("Output coordinate system must be a projected coordinate system.") elif outCoordSys.linearUnitName != "Meter": parameters[11].setErrorMessage("Output coordinate system must have a linear unit code of 'Meter.'") else: parameters[11].clearMessage() ### # Execute the tool. ### def execute(self, parameters, messages): points = parameters[0].valueAsText networkDataset = parameters[1].valueAsText numBands = parameters[2].value begDist = parameters[3].value distInc = parameters[4].value snapDist = parameters[5].value outNetKLoc = parameters[6].valueAsText outRawODCMFCName = parameters[7].valueAsText outRawFCName = parameters[8].valueAsText outAnlFCName = parameters[9].valueAsText numPermsDesc = parameters[10].valueAsText numPerms = self.kfHelper.getPermutationSelection()[numPermsDesc] outCoordSys = parameters[11].value numPointsFieldName = parameters[12].value ndDesc = arcpy.Describe(networkDataset) gkfSvc = GlobalKFunctionSvc() # Refer to the note in the NetworkDatasetLength tool. if outCoordSys is None: outCoordSys = ndDesc.spatialReference messages.addMessage("\nOrigin points: {0}".format(points)) messages.addMessage("Network dataset: {0}".format(networkDataset)) messages.addMessage("Number of distance bands: {0}".format(numBands)) messages.addMessage("Beginning distance: {0}".format(begDist)) messages.addMessage("Distance increment: {0}".format(distInc)) messages.addMessage("Snap distance: {0}".format(snapDist)) messages.addMessage("Output location (database path): {0}".format(outNetKLoc)) messages.addMessage("Raw ODCM data table: {0}".format(outRawODCMFCName)) messages.addMessage("Raw global-K data table (raw analysis data): {0}".format(outRawFCName)) messages.addMessage("Global-K summary data (plottable data): {0}".format(outAnlFCName)) messages.addMessage("Number of random permutations: {0}".format(numPerms)) messages.addMessage("Network dataset length projected coordinate system: {0}".format(outCoordSys.name)) messages.addMessage("Number of Points Field Name: {0}\n".format(numPointsFieldName)) # Calculate the length of the network. networkLength = self.kfHelper.calculateLength(networkDataset, outCoordSys) messages.addMessage("Total network length: {0}".format(networkLength)) # Count the number of crashes. numPoints = self.kfHelper.countNumberOfFeatures(os.path.join(outNetKLoc, points)) # Set up a cutoff lenght for the ODCM data if possible. (Optimization.) cutoff = gkfSvc.getCutoff(numBands, distInc, begDist) # The results of all the calculations end up here. netKCalculations = [] # Use a mutable container for the number of bands so that the below callback # can write to it. The "nonlocal" keyword not available in Python 2.x. numBandsCont = [numBands] # Callback function that does the Network K calculation on an OD cost matrix. def doNetKCalc(odDists, iteration): # Do the actual network k-function calculation. netKCalc = NetworkKCalculation(networkLength, numPoints, odDists, begDist, distInc, numBandsCont[0]) netKCalculations.append(netKCalc.getDistanceBands()) # If the user did not specifiy a number of distance bands explicitly, # store the number of bands. It's computed from the observed data. if numBandsCont[0] is None: numBandsCont[0] = netKCalc.getNumberOfDistanceBands() # Generate the ODCM permutations, including the ODCM for the observed data. # doNetKCalc is called on each iteration. randODCMPermSvc = RandomODCMPermutationsSvc() randODCMPermSvc.generateODCMPermutations("Global Analysis", points, points, networkDataset, snapDist, cutoff, outNetKLoc, outRawODCMFCName, numPerms, outCoordSys, numPointsFieldName, messages, doNetKCalc) # Store the raw analysis data. messages.addMessage("Writing raw analysis data.") gkfSvc.writeRawAnalysisData(outNetKLoc, outRawFCName, netKCalculations) # Analyze the data and store the results. messages.addMessage("Analyzing data.") gkfSvc.writeAnalysisSummaryData(numPerms, netKCalculations, outNetKLoc, outAnlFCName)
class RandomODCMPermutationsSvc: ### # Initialize the service (stateless). ### def __init__(self): self.kfHelper = KFunctionHelper() ### # Generate the ODCM permutations. # @param analysisType Either Global Analysis or Cross Analysis. # @param srcPoints The source points. # @param destPoints The destination points. Ignored if analysisType is GLOBAL. # @param networkDataset The network dataset to use for the ODCMs. # @param snapDist The snap distance for points that are not directly on the network. # @param cutoff The cutoff distance. Ignored if None. # @param outLoc The full path to a database. The ODCM data will be written here. # @param outFC The name of the feature class in outLoc where the ODCM data will be written. # @param numPerms The number of permutations (string representation). # @param outCoordSys The coordinate system to project the points into (optional). # @param numPointsFieldName The optional name of a field in the network dataset's edge source # from which the number of random points should be derived. # @param messages A messages instances with addMessage() implemented (for debug output). # @param callback A callback function(odDists, iteration) called on each iteration # with the current OD cost matrix. ### def generateODCMPermutations(self, analysisType, srcPoints, destPoints, networkDataset, snapDist, cutoff, outLoc, outFC, numPerms, outCoordSys, numPointsFieldName, messages, callback = None): # Default no-op for the callback. if callback is None: callback = lambda odDists, iteration: None # For global analysis the destination points are the same as the source points # (e.g. destPoints is ignored). if analysisType == "GLOBAL" or destPoints is None: destPoints = srcPoints # Count the number of crashes (there may be fewer points in the ODCM, but it's the total # number of crashes that is needed). numDests = self.kfHelper.countNumberOfFeatures(os.path.join(outLoc, destPoints)) messages.addMessage("Number of crashes: {0}".format(numDests)) # Make the observed ODCM and calculate the distance between each set of # points. If a cross analysis is selected, find the distance between the # source and destination points. Otherwise there is only one set of points odDists = self._calculateDistances(networkDataset, srcPoints, destPoints, snapDist, cutoff) self._writeODCMData(odDists, outLoc, outFC, 0) callback(odDists, 0) messages.addMessage("Iteration 0 (observed) complete.") # Generate the OD Cost matrix permutations. kfTimer = KFunctionTimer(numPerms) for i in range(1, numPerms + 1): if numPointsFieldName: randPoints = self.kfHelper.generateRandomPoints(networkDataset, outCoordSys, None, numPointsFieldName) else: randPoints = self.kfHelper.generateRandomPoints(networkDataset, outCoordSys, numDests, None) # See the note above: Either find the distance from the source points to the random points, # or the distance between the random points. if analysisType == "CROSS": odDists = self._calculateDistances(networkDataset, srcPoints, randPoints, snapDist, cutoff) else: odDists = self._calculateDistances(networkDataset, randPoints, randPoints, snapDist, cutoff) self._writeODCMData(odDists, outLoc, outFC, i) callback(odDists, i) # Clean up the random points table. arcpy.Delete_management(randPoints) # Show the progress. kfTimer.increment() messages.addMessage("Iteration {0} complete. Elapsed time: {1}s. ETA: {2}s.".format( i, kfTimer.getElapsedTime(), kfTimer.getETA())) ### # Calculate the distances between each set of points using an OD Cost Matrix. # @param networkDataset A network dataset which the points are on. # @param srcPoints The source points to calculate distances from. # @param destPoints The destination points to calculate distances to. # @param snapDist If a point is not directly on the network, it will be # snapped to the nearset line if it is within this threshold. # @param cutoff The cutoff distance for the ODCM (optional). ### def _calculateDistances(self, networkDataset, srcPoints, destPoints, snapDist, cutoff): # This is the current map, which should be an OSM base map. curMapDoc = arcpy.mapping.MapDocument("CURRENT") # Get the data from from the map (see the DataFrame object of arcpy). dataFrame = arcpy.mapping.ListDataFrames(curMapDoc, "Layers")[0] # Create the cost matrix. costMatResult = arcpy.na.MakeODCostMatrixLayer(networkDataset, "TEMP_ODCM_NETWORK_K", "Length", cutoff) odcmLayer = costMatResult.getOutput(0) # The OD Cost Matrix layer will have Origins and Destinations layers. Get # a reference to each of these. odcmSublayers = arcpy.na.GetNAClassNames(odcmLayer) odcmOriginLayer = odcmSublayers["Origins"] odcmDestLayer = odcmSublayers["Destinations"] # Add the origins and destinations to the ODCM. arcpy.na.AddLocations(odcmLayer, odcmOriginLayer, srcPoints, "", snapDist) arcpy.na.AddLocations(odcmLayer, odcmDestLayer, destPoints, "", snapDist) # Solve the matrix. arcpy.na.Solve(odcmLayer) # Show the ODCM layer (it must be showing to open th ODLines sub layer below). #arcpy.mapping.AddLayer(dataFrame, odcmLayer, "TOP") #arcpy.RefreshTOC() # Get the "Lines" layer, which has the distance between each point. odcmLines = arcpy.mapping.ListLayers(odcmLayer, odcmSublayers["ODLines"])[0] # This array will hold all the OD distances. odDists = [] if srcPoints == destPoints: # If the source points and destination points are the same, exclude the # distance from the point to itself. where = """{0} <> {1}""".format( arcpy.AddFieldDelimiters(odcmLines, "originID"), arcpy.AddFieldDelimiters(odcmLines, "destinationID")) else: where = "" with arcpy.da.SearchCursor( in_table=odcmLines, field_names=["Total_Length", "originID", "destinationID"], where_clause=where) as cursor: for row in cursor: odDists.append({"Total_Length": row[0], "OriginID": row[1], "DestinationID": row[2]}) return odDists ### # Write the ODCM data to a table. # @param odDists An array of raw ODCM data. # @param outLoc The location of a database. # @param outFC The feature class name, in outLoc, to write the data to. # @param iteration The iteration number (0 is observed). ### def _writeODCMData(self, odDists, outLoc, outFC, iteration): # This is the full path to the output feature class. outFCFullPath = os.path.join(outLoc, outFC) if iteration == 0: # Create the output table. arcpy.CreateTable_management(outLoc, outFC) arcpy.AddField_management(outFCFullPath, "Iteration_Number", "LONG") arcpy.AddField_management(outFCFullPath, "OriginID", "LONG") arcpy.AddField_management(outFCFullPath, "DestinationID", "LONG") arcpy.AddField_management(outFCFullPath, "Total_Length", "DOUBLE") with arcpy.da.InsertCursor(outFCFullPath, ["Iteration_Number", "OriginID", "DestinationID", "Total_Length"]) as cursor: for odDist in odDists: cursor.insertRow([iteration, odDist["OriginID"], odDist["DestinationID"], odDist["Total_Length"]])