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