3

I'm unable to find an example of an ArcGIS Python Toolbox similar to the one I'm trying to get to work. The standalone python script works beautifully. Turning it into a Python Toolbox so I can use it in model builder / GP tool is where I'm having problems.

I'd like to be able to connect a point feature layer and a polyline feature layer to this tool as inputs, do some calculations, store the results in fields in the input point feature layer, and have the results available as an output feature layer. EDIT: My python script doesn't seem to have any output, per se. My thinking now is that I need the python script to create a feature class with all the fields, and then calculate them in the output (rather than the input).

The first input parameter, in_features should be a point feature set. The second input parameter is a single polyline, which in my case is a street centerline. The tool calculates station, offset and orientation values and places the results in their corresponding fields in the output feature layer. This means I can specify the output layer as "derived", right?

ArcCatalog reports no syntax errors. I get the message "This tool has no parameters" when trying to run the tool. The goal is to use this tool as a GP tool in ArcGIS Server. I'd like to be able to drop it into model builder and hook up the inputs and output.

Of course all this could be avoided if ESRI made the Near tool available to ArcGIS Standard license holders. Anyway, here's my CalculateStationOffset.pyt file:


Maybe I'm going about this all wrong. Perhaps I should be creating an output feature class in my Python code and calculating fields there. If I want the results to show up via the Web AppBuilder Geoprocessing widget, anyway. That's a whole other issue.

import arcpy

class Toolbox(object):
    def __init__(self):
        self.label = "Station Offset toolbox"
        self.alias = "StationOffset"

        # List of tool classes associated with this toolbox
        self.tools = [CalculateStationOffset]

class CalculateStationOffset(object):
    def __init__(self):
        self.label = "Calculate Station Offset"
        self.description = "Calculate Station Offset"

def getParameterInfo(self):
    #Define parameter definitions

    # Input Features parameter
    in_features = arcpy.Parameter(
        displayName="Input Features",
        name="in_features",
        datatype="GPFeatureLayer",
        parameterType="Required",
        direction="Input")

    in_features.filter.list = ["Point"]

    # Alignment parameter
    in_alignment = arcpy.Parameter(
        displayName="Alignment",
        name="in_alignment",
        datatype="GPFeatureLayer",
        parameterType="Required",
        direction="Input")

    in_alignment.filter.list = ["Polyline"]

    # Derived Output Features parameter
    out_features = arcpy.Parameter(
        displayName="Output Features",
        name="out_features",
        datatype="GPFeatureLayer",
        parameterType="Derived",
        direction="Output")

    out_features.parameterDependencies = [in_features.name]
    out_features.schema.clone = True

    parameters = [in_features, in_alignment, out_features] 

    return parameters

    def isLicensed(self):
        return True

    def updateParameters(self, parameters):
        if parameters[0].altered:
            parameters[1].value = arcpy.ValidateFieldName(parameters[1].value, parameters[0].value)
        return

    def updateMessages(self, parameters):
        return

def execute(self, parameters, messages):
    fc_pnt         = parameters[0].valueAsText
    fc_line        = parameters[1].valueAsText

    # Fields
    fields = ['SHAPE@', 'MEAS', 'Distance', 'NEAR_X', 'NEAR_Y', 'Orient', 'POINT_X', 'POINT_Y', 'NEAR_ANGLE']

    # Get line geometry - assumes only one feature in feature class
    polyline = arcpy.da.SearchCursor(fc_line, "SHAPE@").next()[0]

    # Loop over the point feature class
    with arcpy.da.UpdateCursor(fc_pnt, fields) as cursor:
        for row in cursor:
            dist=polyline.queryPointAndDistance(row[0], False)
            row[1] = dist[1]
            row[2] = dist[2]
            row[3] = dist[0].centroid.X
            row[4] = dist[0].centroid.Y
            if dist[3] == 0:
                row[5]="Left"
            else:
                row[5]="Right"
            row[6] = row[0].centroid.X
            row[7] = row[0].centroid.Y
            print(dist)
            cursor.updateRow(row)
  • Is your problem in creating the tool with parameters? have a read of desktop.arcgis.com/en/arcmap/10.3/analyze/creating-tools/… that talks about parameters... – Michael Stimson Apr 26 '16 at 22:13
  • Thought I would try this using a Python Toolbox as it is supposed to be the new way of doing things. I've been all over the help but there just aren't enough examples. desktop.arcgis.com/en/arcmap/10.3/analyze/creating-tools/… – Rayner Apr 27 '16 at 6:58
  • Maybe I'm better off working with a script tool instead, as long as I can hook up a feature set as one of the input parameters. This is so the user can interactively input one or more points by clicking on the data frame. Didn't think the Python Toolbox would be so difficult to work with. – Rayner Apr 27 '16 at 7:08
  • I think you are getting mixed up on what type of parameter to use. The difference between a script tool and a python toolbox isn't huge, and it's actually possible to write code that can be used as either. A derived parameter is used when the user has no input on what the parameter actually is. Either the tool automatically creates it (name and all) based on input data, or the output is one of the inputs, just modified. The output of AddField is a derived parameter, since the output is actually just the input layer, with an additional field. – Evil Genius Apr 27 '16 at 11:23
  • 2
    Is the spacing in your example code in the post the same as your actual code? If so, you're getting the no parameters error because your Python syntax is likely valid, but the getParameterInfo, execute, and other methods are standalone methods and not part of the CalculateStationObject class. They need to be indented to be part of the class. – Josh Werts Apr 27 '16 at 12:17
2

The output parameter, when I've used it, seems to really only work to add the output as a layer to the map document the tool has been called in.

I noticed something that may be giving you errors in your code:

if parameters[0].altered:
     parameters[1].value = arcpy.ValidateFieldName(parameters[1].value, parameters[0].value)

parameter[1] is equivalent to in_alignment which is a feature layer. The ValidateFieldName tool requires the name of a field as a string. The tool is meant to check that a string can be used as a field name in a particular feature class.

Also, the steps you are currently doing the execute, is you are updating the input point feature class with all the attributes. This presumes that the list of fields are in the point feature class.

1

So the answer to my original question "Is it possible to have two input feature layers and a derived output feature layer in an ArcGIS Python Toolbox tool?" is of course yes.

Finally got the Python Toolbox to work. Was a combination of parts of my code not being indented correctly and working out the correct input and output tools in model builder to expose the parameters to the geoprocessing widget in Web AppBuilder. Next step is to figure out how to get it to do the calculation in TN State Plane NAD83 rather than Web Mercator (auxiliary sphere). Thanks everyone for your help.

import arcpy

class Toolbox(object):
    def __init__(self):
        self.label = "Station Offset toolbox"
        self.alias = "StationOffset"

        # List of tool classes associated with this toolbox
        self.tools = [CalculateStationOffset]

class CalculateStationOffset(object):
    def __init__(self):
        self.label = "Calculate Station Offset"
        self.description = "Calculate Station Offset"

    def getParameterInfo(self):
        #Define parameter definitions

        # Input Features parameter
        in_features = arcpy.Parameter(
            displayName="Input Features",
            name="in_features",
            datatype="GPFeatureLayer",
            parameterType="Required",
            direction="Input")

        in_features.filter.list = ["Point"]

        # Alignment parameter
        in_alignment = arcpy.Parameter(
            displayName="Alignment",
            name="in_alignment",
            datatype="GPFeatureLayer",
            parameterType="Required",
            direction="Input")

        in_alignment.filter.list = ["Polyline"]

        # Derived Output Features parameter
        out_features = arcpy.Parameter(
            displayName="Output Features",
            name="out_features",
            datatype="GPFeatureLayer",
            parameterType="Derived",
            direction="Output")

        out_features.parameterDependencies = [in_features.name]
        out_features.schema.clone = True

        parameters = [in_features, in_alignment, out_features] 

        return parameters

    def isLicensed(self):
        return True

    def updateParameters(self, parameters):
        if parameters[0].altered:
            parameters[1].value = arcpy.ValidateFieldName(parameters[1].value, parameters[0].value)
        return

    def updateMessages(self, parameters):
        return

    def execute(self, parameters, messages):
        fc_pnt         = parameters[0].valueAsText
        fc_line        = parameters[1].valueAsText

        # Fields
        fields = ['SHAPE@', 'MEAS', 'Distance', 'NEAR_X', 'NEAR_Y', 'Orient', 'POINT_X', 'POINT_Y', 'NEAR_ANGLE']

        # Get line geometry - assumes only one feature in feature class
        polyline = arcpy.da.SearchCursor(fc_line, "SHAPE@").next()[0]

        # Loop over the point feature class
        with arcpy.da.UpdateCursor(fc_pnt, fields) as cursor:
            for row in cursor:
                dist=polyline.queryPointAndDistance(row[0], False)
                row[1] = dist[1]
                row[2] = dist[2]
                row[3] = dist[0].centroid.X
                row[4] = dist[0].centroid.Y
                if dist[3] == 0:
                    row[5]="Left"
                else:
                    row[5]="Right"
                row[6] = row[0].centroid.X
                row[7] = row[0].centroid.Y
                print(dist)
                cursor.updateRow(row)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.