Select to view content in your preferred language

Script Tool with Optional Input Parameters and Parameter Validation

488
10
Jump to solution
3 weeks ago
ewmahaffey
Occasional Contributor

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

 

0 Kudos
1 Solution

Accepted Solutions
PandiyanKesavan
Occasional Contributor

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

View solution in original post

10 Replies
DavidPike
MVP Notable Contributor

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




ewmahaffey
Occasional Contributor

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.

0 Kudos
DavidPike
MVP Notable Contributor

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.

PandiyanKesavan
Occasional Contributor

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
HaydenWelch
MVP Regular Contributor

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): ...
ewmahaffey
Occasional Contributor

Thanks @PandiyanKesavan 

I swapped out my validation code with yours, and it seems like it's still expecting an input value for the FeatureSet when I try to manually enter the Lat/Longs. It worked fine when I selected the interactive click entry that produces the FeatureSet.  Here's the error:

ewmahaffey_1-1748445632052.png

 

 

0 Kudos
PandiyanKesavan
Occasional Contributor

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
ewmahaffey
Occasional Contributor

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
0 Kudos
ewmahaffey
Occasional Contributor

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

 

0 Kudos