Here's the code for a python toolbox I came up with to randomly place a finite number of sets of circles (whose centers are, naturally, points) with different radii into a finite number of arbitrarily shaped polygons. The code is based on the seed Paul provided in his answer above. The parameter param1
is a GPValueTable
data type, which allows you to specify the number of columns and each column's own data type. It's actually just a list of lists, but each list can have multiple data types in it.
The idea for the problem was based on figuring out spacing for placing plants which require a minimum spacing from other plants, but each species could have a different spacing. . .thus the language centered around "Species".
The way the tool is currently set up produces a feature class of only circles, but point feature classes are created, and it is easy enough to remark out the lines of code that delete the point feature classes.
There is some validation included, which keeps you from executing the tool unless the radii/spacing is in descending order in the value table.
It works fine as a brute force tool, but only if the workspace is a geodatabase (not just a folder), and I can't figure out how to add the resulting feature class to a dataframe in the current .mxd using this python toolbox. It's probably a bit clunky, but it's my first try.
import arcpy
import arcpy.mapping
import os.path
class Toolbox(object):
def __init__(self):
"""Define the toolbox (the name of the toolbox is the name of the
.pyt file)."""
self.label = "Restoration Planting Toolbox"
self.alias = ""
# List of tool classes associated with this toolbox
self.tools = [SpeciesSpacing]
class SpeciesSpacing(object):
def __init__(self):
"""Define the tool (tool name is the name of the class)."""
self.label = "Species Spacing Tool"
self.description = "This tool takes a list of species, number of individuals in each species, their minimum spacing, and a bounding polygon and distributes circles within
the polygon such that no species' minimum spacing is violated."
self.canRunInBackground = False
def getParameterInfo(self):
"""Define parameter definitions"""
# First parameter
param0 = arcpy.Parameter(
displayName="Bounding polygon feature class:",
name="in_features",
datatype="GPLayer",
parameterType="Required",
direction="Input")
# Second parameter
param1 = arcpy.Parameter(
displayName="Add each species by name ***LIST SPECIES IN ORDER OF DESCENDING MINIMUM SPACING***:",
name="species_table",
datatype="GPValueTable",
parameterType="Required",
direction="Input")
param1.columns = [['String','Species Name'],['Long','Number of Individuals'],['Long','Minimum Spacing (feet)']]
param2 = arcpy.Parameter(
displayName="Output workspace:",
name="output_ws",
datatype="DEWorkspace",
parameterType="Required",
direction="Input")
param3 = arcpy.Parameter(
displayName="Name of output feature class:",
name="output_fc",
datatype="GPString",
parameterType="Required",
direction="Input")
parameters = [param0,param1,param2,param3]
return parameters
def isLicensed(self):
"""Set whether tool is licensed to execute."""
return True
def updateParameters(self, parameters):
"""Modify the values and properties of parameters before internal
validation is performed. This method is called whenever a parameter
has been changed."""
if parameters[1].values is not None:
vtCheck = parameters[1].values
for i in range(len(vtCheck)-1):
if vtCheck[i][2] < vtCheck[i+1][2]:
parameters[2].enabled = False
parameters[3].enabled = False
elif vtCheck[i][2] >= vtCheck[i+1][2]:
parameters[2].enabled = True
parameters[3].enabled = True
return
def updateMessages(self, parameters):
"""Modify the messages created by internal validation for each tool
parameter. This method is called after internal validation."""
return
def execute(self, parameters, messages):
"""The source code of the tool."""
# Specify the current map
mxd = arcpy.mapping.MapDocument("CURRENT")
# get the data frame
df = arcpy.mapping.ListDataFrames(mxd,"*")[0]
# Specify input polygon
inputPoly = parameters[0].valueAsText
# Set output path
outpath = parameters[2].valueAsText
arcpy.env.workspace = outpath
# Specify final feature class name
outname = parameters[3].valueAsText
fullpath = os.path.join(outpath,outname)
messages.addMessage("\nOutput is located at {0}".format(fullpath))
# Get values from value table parameter
vt=parameters[1].values
# Make a copy of the area
arcpy.CopyFeatures_management(inputPoly,"area0")
# Get the spatial reference of the input polygon
sr = arcpy.Describe(inputPoly).spatialReference
# Make polygon feature class to place circles in
arcpy.CreateFeatureclass_management(outpath,outname,"POLYGON",spatial_reference=sr)
# Add field for species name
arcpy.AddField_management(outname,"Species","text")
# Create the initial set of random points
arcpy.CreateRandomPoints_management(outpath, "{0}_points".format(vt[0][0]), "area0", "",
vt[0][1], "{0} feet".format(vt[0][2]))
for i in range(len(vt)):
# Create buffers
arcpy.Buffer_analysis(outpath+"/{0}_points".format(vt[i][0]), "{0}_buffers".format(vt[i][0]),
"{0} feet".format(vt[i][2]), dissolve_option="NONE")
messages.addMessage("\nPlacing {0}. . .".format(vt[i][0]))
# Add 'Species' field
arcpy.AddField_management("{0}_buffers".format(vt[i][0]),"Species","text")
# Calculate 'Species' field based on value table input
arcpy.CalculateField_management("{0}_buffers".format(vt[i][0]),"Species",'"{0}"'.format(vt[i][0]))
# Erase buffers from 'area{i}' to make 'area{i+1}'
arcpy.Erase_analysis("area{0}".format(i), "{0}_buffers".format(vt[i][0]), "area{0}".format(i+1))
# Append '{species}_buffers' to 'randomized_planting' feature class
arcpy.Append_management("{0}_buffers".format(vt[i][0]),outname,"NO_TEST")
# Delete 'area{i}' feature class
arcpy.Delete_management(outpath+"/area{0}".format(i))
messages.addMessage("\nCleaning up. . .")
# Delete '{species}_buffers' feature class
arcpy.Delete_management(outpath+"/{0}_buffers".format(vt[i][0]))
# Delete '{species}_points' feature class
arcpy.Delete_management(outpath+"/{0}_points".format(vt[i][0]))
# Generate random points in the next iteration of the area (except for the final iteration)
if i < len(vt)-1:
arcpy.CreateRandomPoints_management(outpath, "{0}_points".format(vt[i+1][0]), "area{0}".format(i+1), "",
vt[i+1][1], "{0} feet".format(vt[i+1][2]))
# Delete final iteration of the area
arcpy.Delete_management(outpath+"/area{0}".format(len(vt)))
messages.addMessage("\nWait for it. . .")
newLayer = arcpy.MakeFeatureLayer_management(fullpath, "Random")
add_layer = arcpy.mapping.Layer("Random")
arcpy.mapping.AddLayer(df,add_layer)
# Refresh things
arcpy.RefreshActiveView()
arcpy.RefreshTOC()
#del mxd, df
return