I'm trying to understand how to build a script tool that has multiple optional input parameters in conjunction with parameter validation. Basically, I give the user two options to input a location that gets fed into a buffer tool. The first input option is to use an interactive map click using a feature set on the back end. The second option is to allow the user to manually enter coordinates as the input location for the buffer tool. When I set up parameter validation to disable the option not being used, the code behind still looks for the parameter/s even though the input has been disabled. I can't figure out how to get it to ignore the parameters that are not being entered by the disabled input options. Any advice would be much appreciated.
Here's the main script behind the tool:
import arcpy
arcpy.overwriteOutput = True
output_SR = arcpy.SpatialReference(4326)
output_gdb = "%scratchGDB%"
location_by_featureset = arcpy.GetParameter(0)
if location_by_featureset:
Buffer = arcpy.analysis.MultipleRingBuffer(location_by_featureset, output_gdb+"\Output_Buffer", "20", "Miles")
else:
Lat_Int = int(arcpy.GetParameterAsText(1))
Long_Int = int(arcpy.GetParameterAsText(2))
point_location = arcpy.management.CreateFeatureclass(output_gdb, "Point_Loc", "Point", "", "Enabled", "Enabled", output_SR)
point_loc = arcpy.Point(Long_Int, Lat_Int)
with arcpy.da.InsertCursor(point_location, ["SHAPE@XY"]) as cursor:
cursor.insertRow([point_loc])
del cursor
Buffer = arcpy.analysis.MultipleRingBuffer(point_location, output_gdb+"\Output_Buffer", "20", "Miles")
arcpy.SetParameter(3, Buffer)
arcpy.AddMessage("Process Complete")
Here's the parameter validation code:
class ToolValidator:
# Class to add custom behavior and properties to the tool and tool parameters.
def __init__(self):
# Set self.params for use in other validation methods.
self.params = arcpy.GetParameterInfo()
def initializeParameters(self):
# Customize parameter properties. This method gets called when the
# tool is opened.
return
def updateParameters(self):
# Modify the values and properties of parameters before internal
# validation is performed.
if self.params[0].altered:
self.params[1].enabled = False
self.params[2].enabled = False
if self.params[1].altered:
self.params[0].enabled = False
return
def updateMessages(self):
# Modify the messages created by internal validation for each tool
# parameter. This method is called after internal validation.
return
# def isLicensed(self):
# # Set whether the tool is licensed to execute.
# return True
# def postExecute(self):
# # This method takes place after outputs are processed and
# # added to the display.
# return
Solved! Go to Solution.
Try the below code, Hope this works for you
import arcpy
arcpy.overwriteOutput = True
output_SR = arcpy.SpatialReference(4326)
output_gdb = arcpy.env.scratchGDB
# Get parameters
input_featureset = arcpy.GetParameter(0) # FeatureSet (optional)
lat_text = arcpy.GetParameterAsText(1) # Latitude (optional)
lon_text = arcpy.GetParameterAsText(2) # Longitude (optional)
output_buffer = arcpy.GetParameterAsText(3) # Output feature class
geometry = None
# Option 1: Map click
if input_featureset:
try:
features = [row[0] for row in arcpy.da.SearchCursor(input_featureset, ["SHAPE@"])]
if features:
geometry = features[0]
arcpy.AddMessage("Using location from map click.")
except:
pass
# Option 2: Manual coordinate input
if not geometry and lat_text and lon_text:
try:
lat = float(lat_text)
lon = float(lon_text)
geometry = arcpy.PointGeometry(arcpy.Point(lon, lat), output_SR)
arcpy.AddMessage("Using manually entered coordinates.")
except:
arcpy.AddError("Invalid manual coordinates. Must be valid decimal numbers.")
raise arcpy.ExecuteError
if not geometry:
arcpy.AddError("No valid input location provided. Provide either a map click or coordinates.")
raise arcpy.ExecuteError
# Create in-memory point feature class
point_fc = arcpy.management.CreateFeatureclass("in_memory", "point_temp", "POINT", spatial_reference=output_SR)
with arcpy.da.InsertCursor(point_fc, ["SHAPE@"]) as cursor:
cursor.insertRow([geometry])
# Run Multiple Ring Buffer
arcpy.AddMessage("Running Multiple Ring Buffer...")
buffer_result = arcpy.analysis.MultipleRingBuffer(point_fc, output_buffer, ["20"], "Miles")
arcpy.AddMessage("Buffer complete. Output saved at: " + buffer_result.getOutput(0))
arcpy.SetParameter(3, buffer_result)
class ToolValidator:
def __init__(self):
self.params = arcpy.GetParameterInfo()
def updateParameters(self):
# Get parameter values
feature_set = self.params[0].value
lat = self.params[1].value
lon = self.params[2].value
# If FeatureSet is used, disable lat/lon
if feature_set:
self.params[1].enabled = False
self.params[2].enabled = False
# If Lat or Lon is used, disable FeatureSet
elif lat or lon:
self.params[0].enabled = False
else:
# If nothing is entered, keep all enabled
self.params[0].enabled = True
self.params[1].enabled = True
self.params[2].enabled = True
def updateMessages(self):
return
The code looks fine at first glance. I think the validation might be throwing things off and setting param 1 and 2 existence to True.
If it were me - I'd just set all the params to optional and use the messaging function in the validation.
I'd also throw in some AddMessage() s to figure out what might be going on if still not working as expected.
class ToolValidator:
# Class to add custom behavior and properties to the tool and tool parameters.
def __init__(self):
# Set self.params for use in other validation methods.
self.params = arcpy.GetParameterInfo()
def initializeParameters(self):
# Customize parameter properties. This method gets called when the
# tool is opened.
return
def updateParameters(self):
# Modify the values and properties of parameters before internal
# validation is performed.
def updateMessages(self):
# Modify the messages created by internal validation for each tool
# parameter. This method is called after internal validation.
return
feature_set_input = self.params[0].value
x_coord_input = self.params[1].value
y_coord_input = self.params[2].value
if not feature_set_input and (x_coord_input is not None or y_coord_input is not None) :
self.params[0].setErrorMessage("Warning - enter interactive feature or X and Y, not both. Interactive feature input will take precedence if both supplied")
self.params[1].setErrorMessage("Warning - enter interactive feature or X and Y, not both. Interactive feature input will take precedence if both supplied")
self.params[2].setErrorMessage("Warning - enter interactive feature or X and Y, not both. Interactive feature input will take precedence if both supplied")
# def isLicensed(self):
# # Set whether the tool is licensed to execute.
# return True
# def postExecute(self):
# # This method takes place after outputs are processed and
# # added to the display.
# return
Thanks David. I have all of the input parameters set as optional, but for whatever reason the tool still expects the the FeatureSet input param (0) to be populated if I choose to manually enter the coordinates. I think I need to use some more conditional statements, but I haven't figured that out yet.
I think what might actually be happening is that the if is always evaluating to True, probably because the feature set relies on a template/schema (I think).
The Try Except you've got as a workaround isn't recommended and a bit hacky.
I'd do an AddMessage to see the value of your parameter(0) each time, and if it is always True, you could just switch the logic to evaluate against the Lat Long params instead.
Your code looks good; logic enables and disables correctly, but it doesn't re-enable parameters.
Try this below code for validation
class ToolValidator:
# Class to add custom behavior and properties to the tool and tool parameters.
def __init__(self):
# Set self.params for use in other validation methods.
self.params = arcpy.GetParameterInfo()
def initializeParameters(self):
# Customize parameter properties. This method gets called when the
# tool is opened.
return
def updateParameters(self):
# If FeatureSet is altered, disable manual inputs
if self.params[0].altered and self.params[0].value:
self.params[1].enabled = False # Latitude
self.params[2].enabled = False # Longitude
# If Latitude or Longitude is altered, disable FeatureSet
elif (self.params[1].altered and self.params[1].value) or (self.params[2].altered and self.params[2].value):
self.params[0].enabled = False
else:
# Re-enable all if nothing is filled out
self.params[0].enabled = True
self.params[1].enabled = True
self.params[2].enabled = True
def updateMessages(self):
return
# def isLicensed(self):
# # Set whether the tool is licensed to execute.
# return True
# def postExecute(self):
# # This method takes place after outputs are processed and
# # added to the display.
# return
This is a bit of an over-answer, but have you considered using pyt/python toolboxes?
from pathlib import Path
from arcpy import (
Parameter,
SpatialReference,
Point,
PointGeometry,
AddMessage,
AddWarning,
AddError,
EnvManager,
)
from arcpy.mp import ArcGISProject
from arcpy.da import SearchCursor, InsertCursor
from arcpy.management import CopyFeatures
from arcpy.analysis import Buffer
class Toolbox(object):
def __init__(self):
self.label = "Point Tools"
self.alias = "PointTools"
self.tools: list[type] = [BufferLoc]
class BufferLoc:
def __init__(self) -> None:
self.description = "Buffers a specified Location by the specified distance"
self.label = "Buffer Loc"
# Tool Constants
self.reference = SpatialReference(4326)
self.project = ArcGISProject('CURRENT')
self.scratch = self.project.defaultGeodatabase
return
def getParameterInfo(self):
features = Parameter(
name="features",
displayName="Features",
direction="Input",
datatype="GPFeatureLayer",
parameterType="Optional"
)
features.filter.list = ['Point']
latitude = Parameter(
name="latitude",
displayName="Latitude",
direction="Input",
datatype="GPDouble",
parameterType="Optional",
)
longitude = Parameter(
name="longitude",
displayName="Longitude",
direction="Input",
datatype="GPDouble",
parameterType="Optional",
)
buffer_distance = Parameter(
name="buffer_distance",
displayName="Buffer Distance",
direction="Input",
datatype="GPLinearUnit",
parameterType="Required",
)
buffer_distance.value = "20 Miles"
method = Parameter(
name="method",
displayName="Method",
direction="Input",
datatype="GPString",
parameterType="Required",
)
method.filter.list = ['GEODESIC', 'PLANAR']
method.value = 'GEODESIC'
output_database = Parameter(
name="output_database",
displayName="Output Database",
direction="Input",
datatype="DEWorkspace",
parameterType="Required",
)
output_database.value = self.scratch
return [features, latitude, longitude, buffer_distance, method, output_database]
def updateParameters(self, parameters: list[Parameter]) -> None:
params = {p.name: p for p in parameters}
# If features value is set, disable lat/lon
params['latitude'].enabled = not params['features'].value
params['longitude'].enabled = not params['features'].value
# If lat or lon is set, disable features
params['features'].enabled = not (params['latitude'].value or params['longitude'].value)
def updateMessages(self, parameters: list[Parameter]): ...
def isLicensed(self):
return True
def execute(self, parameters: list[Parameter], messages: list) -> None:
params = {p.name: p for p in parameters}
# Assign validated parameters
features = params['features'].value
latitude = params['latitude'].value
longitude = params['longitude'].value
output_database = Path(params['output_database'].valueAsText)
buffer_distance = params['buffer_distance'].value
method = params['method'].value
# Do the Buffer
AddMessage("Buffering Points...")
try:
if not features:
features = CopyFeatures([PointGeometry(Point(longitude, latitude), self.reference)], "memory/temp_points")
with EnvManager(overwriteOutput=True):
res = Buffer(
in_features=features,
out_feature_class=str(output_database / "Output_Buffer"),
buffer_distance_or_field=buffer_distance,
line_side='FULL', # Create Parameter for this?
line_end_type='ROUND', # Create Parameter for this?
dissolve_option='NONE', # Create Parameter for this?
method=method
)
except Exception as e:
AddError(f"Buffering Failed!:\n{e}")
return
AddMessage(f"Process Complete:\n\t{res.getMessages(0)}")
if warn := res.getMessages(1):
AddWarning('\t'+warn)
if err := res.getMessages(2):
AddError('\t'+err)
def postExecute(self): ...
Try the below code, Hope this works for you
import arcpy
arcpy.overwriteOutput = True
output_SR = arcpy.SpatialReference(4326)
output_gdb = arcpy.env.scratchGDB
# Get parameters
input_featureset = arcpy.GetParameter(0) # FeatureSet (optional)
lat_text = arcpy.GetParameterAsText(1) # Latitude (optional)
lon_text = arcpy.GetParameterAsText(2) # Longitude (optional)
output_buffer = arcpy.GetParameterAsText(3) # Output feature class
geometry = None
# Option 1: Map click
if input_featureset:
try:
features = [row[0] for row in arcpy.da.SearchCursor(input_featureset, ["SHAPE@"])]
if features:
geometry = features[0]
arcpy.AddMessage("Using location from map click.")
except:
pass
# Option 2: Manual coordinate input
if not geometry and lat_text and lon_text:
try:
lat = float(lat_text)
lon = float(lon_text)
geometry = arcpy.PointGeometry(arcpy.Point(lon, lat), output_SR)
arcpy.AddMessage("Using manually entered coordinates.")
except:
arcpy.AddError("Invalid manual coordinates. Must be valid decimal numbers.")
raise arcpy.ExecuteError
if not geometry:
arcpy.AddError("No valid input location provided. Provide either a map click or coordinates.")
raise arcpy.ExecuteError
# Create in-memory point feature class
point_fc = arcpy.management.CreateFeatureclass("in_memory", "point_temp", "POINT", spatial_reference=output_SR)
with arcpy.da.InsertCursor(point_fc, ["SHAPE@"]) as cursor:
cursor.insertRow([geometry])
# Run Multiple Ring Buffer
arcpy.AddMessage("Running Multiple Ring Buffer...")
buffer_result = arcpy.analysis.MultipleRingBuffer(point_fc, output_buffer, ["20"], "Miles")
arcpy.AddMessage("Buffer complete. Output saved at: " + buffer_result.getOutput(0))
arcpy.SetParameter(3, buffer_result)
class ToolValidator:
def __init__(self):
self.params = arcpy.GetParameterInfo()
def updateParameters(self):
# Get parameter values
feature_set = self.params[0].value
lat = self.params[1].value
lon = self.params[2].value
# If FeatureSet is used, disable lat/lon
if feature_set:
self.params[1].enabled = False
self.params[2].enabled = False
# If Lat or Lon is used, disable FeatureSet
elif lat or lon:
self.params[0].enabled = False
else:
# If nothing is entered, keep all enabled
self.params[0].enabled = True
self.params[1].enabled = True
self.params[2].enabled = True
def updateMessages(self):
return
Sorry for the delayed response. I had to shift to another task for a couple of days. Anyhow, thanks! This code worked. I tweaked it just a bit so that the output buffer was set as a fixed f-class instead of a text input. I also set the point f-class that's generated by the manual coordinate input to not be stored in memory. This is just so that I can reference the point using either input method. Thanks to everyone for helping be better understand how this all works.
Here's the main script for the tool:
import arcpy
arcpy.overwriteOutput = True
output_SR = arcpy.SpatialReference(4326)
output_gdb = arcpy.env.scratchGDB
# Get parameters
input_featureset = arcpy.GetParameter(0) # FeatureSet (optional)
lat_text = arcpy.GetParameterAsText(1) # Latitude (optional)
lon_text = arcpy.GetParameterAsText(2) # Longitude (optional)
#output_gdb = arcpy.GetParameterAsText(3) # Output feature class
geometry = None
# Option 1: Map click
if input_featureset:
try:
features = [row[0] for row in arcpy.da.SearchCursor(input_featureset, ["SHAPE@"])]
if features:
geometry = features[0]
arcpy.AddMessage("Using location from map click.")
except:
pass
# Option 2: Manual coordinate input
if not geometry and lat_text and lon_text:
try:
lat = float(lat_text)
lon = float(lon_text)
geometry = arcpy.PointGeometry(arcpy.Point(lon, lat), output_SR)
arcpy.AddMessage("Using manually entered coordinates.")
except:
arcpy.AddError("Invalid manual coordinates. Must be valid decimal numbers.")
raise arcpy.ExecuteError
if not geometry:
arcpy.AddError("No valid input location provided. Provide either a map click or coordinates.")
raise arcpy.ExecuteError
# Create in-memory point feature class
point_fc = arcpy.management.CreateFeatureclass(output_gdb, "point_temp", "POINT", spatial_reference=output_SR)
with arcpy.da.InsertCursor(point_fc, ["SHAPE@"]) as cursor:
cursor.insertRow([geometry])
arcpy.SetParameter(4, point_fc)
# Run Multiple Ring Buffer
arcpy.AddMessage("Running Multiple Ring Buffer...")
buffer_result = arcpy.analysis.MultipleRingBuffer(point_fc, output_gdb+"\Output_Buffer", ["20"], "Miles")
arcpy.AddMessage("Buffer complete. Output saved at: " + buffer_result.getOutput(0))
arcpy.SetParameter(3, buffer_result)
Here's the validation script
class ToolValidator:
def __init__(self):
self.params = arcpy.GetParameterInfo()
def updateParameters(self):
# Get parameter values
feature_set = self.params[0].value
lat = self.params[1].value
lon = self.params[2].value
# If FeatureSet is used, disable lat/lon
if feature_set:
self.params[1].enabled = False
self.params[2].enabled = False
# If Lat or Lon is used, disable FeatureSet
elif lat or lon:
self.params[0].enabled = False
else:
# If nothing is entered, keep all enabled
self.params[0].enabled = True
self.params[1].enabled = True
self.params[2].enabled = True
def updateMessages(self):
return
OK, I think I've something working that uses a little of what each of you had recommended. I ended having to use a Try/Except statement in the main script to allow the tool to get past some of the parameters. I'm not sure if it's correct, but it works. I plan to add some defaults for layer symbology, etc. Thanks all!
Main Script:
import arcpy
arcpy.overwriteOutput = True
output_SR = arcpy.SpatialReference(4326)
#output_gdb = "%scratchGDB%"
arcpy.env.workspace = r"C:\Workspace\SMADS\Tool_Migration\Tool_Migration\Default.gdb"
output_gdb = arcpy.env.workspace
try:
location_by_featureset = arcpy.GetParameter(0)
Buffer = arcpy.analysis.MultipleRingBuffer(location_by_featureset, output_gdb+"\Output_Buffer", "20", "Miles")
except:
Lat_Int = int(arcpy.GetParameterAsText(1))
Long_Int = int(arcpy.GetParameterAsText(2))
point_location = arcpy.management.CreateFeatureclass(output_gdb, "Point_Loc", "Point", "", "Enabled", "Enabled", output_SR)
point_loc = arcpy.Point(Long_Int, Lat_Int)
with arcpy.da.InsertCursor(point_location, ["SHAPE@XY"]) as cursor:
cursor.insertRow([point_loc])
del cursor
Buffer = arcpy.analysis.MultipleRingBuffer(point_location, output_gdb+"\Output_Buffer", "20", "Miles")
arcpy.SetParameter(4, point_location)
arcpy.SetParameter(3, Buffer)
arcpy.AddMessage("Process Complete")
Validation Script:
class ToolValidator:
# Class to add custom behavior and properties to the tool and tool parameters.
def __init__(self):
# Set self.params for use in other validation methods.
self.params = arcpy.GetParameterInfo()
def initializeParameters(self):
# Customize parameter properties. This method gets called when the
# tool is opened.
return
def updateParameters(self):
# Modify the values and properties of parameters before internal
# validation is performed.
# If FeatureSet is altered, disable manual inputs
if self.params[0].altered and self.params[0].value:
self.params[1].enabled = False # Latitude
self.params[2].enabled = False # Longitude
# If Latitude or Longitude is altered, disable FeatureSet
elif (self.params[1].altered and self.params[1].value) or (self.params[2].altered and self.params[2].value):
self.params[0].enabled = False
else:
# Re-enable all if nothing is filled out
self.params[0].enabled = True
self.params[1].enabled = True
self.params[2].enabled = True
return
def updateMessages(self):
# Modify the messages created by internal validation for each tool
# parameter. This method is called after internal validation.
return
# def isLicensed(self):
# # Set whether the tool is licensed to execute.
# return True
# def postExecute(self):
# # This method takes place after outputs are processed and
# # added to the display.
# return