Beispiel #1
0
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)
Beispiel #3
0
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"]])