Select to view content in your preferred language

Python to tool

547
8
Jump to solution
09-13-2024 01:46 AM
MordehayAv-revaya
Emerging Contributor

I have a script in python that works well in Arcpro on layers found in sql in cooperation with our portal. When I turn the script into a geoprocessing tool, it doesn't work. It seems it doesn't find the layers it needs if I give a route (maybe an incorrect route). or the layer type is not appropriate. And even when I work with parameters it doesn't work. Is this the forum for this question to upload code? Or another group?

1 Solution

Accepted Solutions
RichardHowe
Frequent Contributor

So in order to add a geoprocessing tool front end to this, you would create a script tool in a toolbox and name it approrpriately.

 

You should have a couple of parameters by my reckoning:

Input lines layer and input points layer

I generally advise people to make the data type as "Feature layer" as this will allow drag/drop inputs from the TOC in Pro, but also allow you to navigate to a feature class via the little folder button. Consider it a best of both worlds input. Both would be of type "Required"

Right-click your tool and edit, pasting your code in.

 

You then need to create reference to your input parameters in the script, so it knows where the data is coming from via the GetParameterAsText function. The number that follows in brackets denotes the order in which your parameters are stored in the tool properties, and as ever python starts counting at zero.

I would suggest lines as below:


line_layer = arcpy.GetParameterAsText(0)
point_layer = arcpy.GetParameterAsText(1)

Just above your #Activate Lines comment

 

And that should be it...

 

View solution in original post

0 Kudos
8 Replies
RichardHowe
Frequent Contributor

Happy to help if you post up what you have

0 Kudos
MordehayAv-revaya
Emerging Contributor

Thanks Richard, Here is the code, the code places a point at the starting point of the line - in the points layer, calculates an angle and transfers information. My problem is just to understand how to make it so that the reference to the layers and the possibility to edit them, will be preserved in the transition from the script in Arcpro to the tool in the portal.

import arcpy
import math

def LinesToPoints(line_layer="TheLines", point_layer="ThePoints"):
# Activate layers
lines = arcpy.management.MakeFeatureLayer(line_layer, "lines_lyr")
points = arcpy.management.MakeFeatureLayer(point_layer, "points_lyr")

# Add fields if they do not exist
required_fields_lines = ["TheAngle", "TransferredToPoint", "PointCode", "PointOrder", "PointNumber"]
required_fields_points = ["TheAngle", "PointCode", "PointOrder", "PointNumber"]

for field in required_fields_lines:
if field not in [f.name for f in arcpy.ListFields("lines_lyr")]:
arcpy.management.AddField(lines, field, "DOUBLE" if field == "TheAngle" else "SHORT")

for field in required_fields_points:
if field not in [f.name for f in arcpy.ListFields("points_lyr")]:
arcpy.management.AddField(points, field, "DOUBLE" if field == "TheAngle" else "SHORT")

# List to store point data for later creation
points_data = []

# Calculate the angle and store the value in the field
with arcpy.da.UpdateCursor(lines, ["SHAPE@", "TheAngle", "TransferredToPoint", "PointCode", "PointOrder", "PointNumber"]) as cursor:
for row in cursor:
line_geom = row[0]
start_point = line_geom.firstPoint
end_point = line_geom.lastPoint

# Calculate the angle
delta_x = end_point.X - start_point.X
delta_y = end_point.Y - start_point.Y
angle = math.degrees(math.atan2(delta_y, delta_x))
row[1] = angle

# Update the TransferredToPoint field
row[2] = 1

cursor.updateRow(row)

# Add the data to the points list
point_geom = arcpy.PointGeometry(start_point)
points_data.append([point_geom, angle, row[3], row[4], row[5]]) # Collecting data for later addition

# Add the points to the ThePoints layer
with arcpy.da.InsertCursor(points, ["SHAPE@", "TheAngle", "PointCode", "PointOrder", "PointNumber"]) as insert_cursor:
for point_data in points_data:
insert_cursor.insertRow(point_data)

print("The code has completed successfully")

# Call the function
LinesToPoints()

 

 

0 Kudos
RichardHowe
Frequent Contributor

So in order to add a geoprocessing tool front end to this, you would create a script tool in a toolbox and name it approrpriately.

 

You should have a couple of parameters by my reckoning:

Input lines layer and input points layer

I generally advise people to make the data type as "Feature layer" as this will allow drag/drop inputs from the TOC in Pro, but also allow you to navigate to a feature class via the little folder button. Consider it a best of both worlds input. Both would be of type "Required"

Right-click your tool and edit, pasting your code in.

 

You then need to create reference to your input parameters in the script, so it knows where the data is coming from via the GetParameterAsText function. The number that follows in brackets denotes the order in which your parameters are stored in the tool properties, and as ever python starts counting at zero.

I would suggest lines as below:


line_layer = arcpy.GetParameterAsText(0)
point_layer = arcpy.GetParameterAsText(1)

Just above your #Activate Lines comment

 

And that should be it...

 

0 Kudos
MordehayAv-revaya
Emerging Contributor

Thanks a lot Richard, I'll try it on Sunday when I get back to work and I'll let you know. Have a great weekend!

HaydenWelch
Frequent Contributor

Went ahead and helped translate your code to a PYT toolbox. Tried to mark up the code with comments and reasons for why I did things in certain ways. Also added progress bar and messaging for you. It's vital when making tools to make sure the user is able to tell that it's actually working and not hung. Especially if it's updating data and will cause errors if cancelled halfway.

import arcpy
import math


class Toolbox:
    def __init__(self) -> None:
        self.label = "Lines to Points"
        self.alias = "Lines to Points"
        self.tools = [LinesToPoints]

class LinesToPoints:
    def __init__(self) -> None:
        self.label = "Lines to Points"
        self.description = "Converts lines to points at a specified interval"
        self.canRunInBackground = False
        #self.category = "Analysis"
        
        self.required_point_fields = ["TheAngle", "PointCode", "PointOrder", "PointNumber"]
        self.required_line_fields = ["TheAngle", "TransferredToPoint", "PointCode", "PointOrder", "PointNumber"]
    
    def getParameterInfo(self) -> list:
        
        in_lines = arcpy.Parameter(
            displayName="Input Lines",
            name="in_lines",
            datatype="DEFeatureClass",
            parameterType="Required",
            direction="Input"
        )
        in_lines.filter.list = ["Polyline"]
        
        in_points = arcpy.Parameter(
            displayName="Input Points",
            name="in_points",
            datatype="DEFeatureClass",
            parameterType="Required",
            direction="Input"
        )
        in_points.filter.list = ["Point"]
        
        return [in_lines, in_points]
    
    def isLicensed(self) -> bool:
        return True
    
    def updateParameters(self, parameters: list) -> None:
        return
    
    def updateMessages(self, parameters: list) -> None:
        return
    
    def execute(self, parameters: list, messages: list) -> None:
        
        # This allows you to re-order the parameters without breaking the code
        # when you use an index you can't re-order the parameters without updating the indexes
        # inthe execute method
        param_dict = {param.name: param for param in parameters}
        
        in_points = param_dict["in_points"].valueAsText
        in_lines = param_dict["in_lines"].valueAsText
        
        in_points_layer = arcpy.management.MakeFeatureLayer(in_points, "memory/in_points_layer")
        in_lines_layer = arcpy.management.MakeFeatureLayer(in_lines, "memory/in_lines_layer")
        
        line_count, *_ = arcpy.management.GetCount(in_lines_layer)
        line_count = int(line_count)
        
        # Add the required fields to the points layer
        point_fields = arcpy.ListFields(in_points_layer)
        for field in filter(lambda fld: fld not in point_fields, self.required_point_fields):
            fld_type = "DOUBLE" if field == "TheAngle" else "SHORT"
            arcpy.management.AddField(in_points_layer, field, fld_type)
        
        # Add the required fields to the lines layer
        line_fields = arcpy.ListFields(in_lines_layer)
        for field in filter(lambda fld: fld not in line_fields, self.required_line_fields):
            fld_type = "DOUBLE" if field == "TheAngle" else "SHORT"
            arcpy.management.AddField(in_lines_layer, field, fld_type)
        
        points_to_insert = []
        
        # Start a Progressor
        arcpy.SetProgressor("step", "Transferring points from lines", 0, line_count, 1)
        with arcpy.da.UpdateCursor(in_lines_layer, ['SHAPE@'] + self.required_line_fields) as cursor:
            for idx, row in enumerate(as_dict(cursor), 1):
                arcpy.SetProgressorLabel(f"Transferring points from line ({idx} of {line_count})")
                
                line_geom: arcpy.Polyline = row["SHAPE@"]
                start_point: arcpy.Point = line_geom.firstPoint
                end_point: arcpy.Point = line_geom.lastPoint
        
            delta_x = end_point.X - start_point.X
            delta_y = end_point.Y - start_point.Y
            angle = math.degrees(math.atan2(delta_y, delta_x))
            row["TheAngle"] = angle
            row["TransferredToPoint"] = 1
            
            # Match the way we're setting up the field list so we can modify the script behavior without
            # worrying about the order of the fields
            
            # Shape@ is always the first field in the list
            point_data = [arcpy.PointGeometry(start_point, line_geom.spatialReference)]
            
            # self.required_point_fields sets the order and name of the remaining fields
            point_data.extend([row[field] for field in self.required_point_fields])
            
            # Need to convert the dictionary values back to a list to update the row
            # This only works in Python 3 because dictionaries are ordered
            try:
                cursor.updateRow(list(row.values()))
            except Exception as e:
                arcpy.AddError(f"Error updating row: {e}")
            arcpy.SetProgressorPosition()
        
        arcpy.ResetProgressor()
        arcpy.SetProgressor("step", "Inserting points", 0, len(points_to_insert), 1)
        with arcpy.da.InsertCursor(in_points_layer, ['SHAPE@'] + self.required_point_fields) as cursor:
            for idx, point in enumerate(points_to_insert, 1):
                arcpy.SetProgressorLabel(f"Inserting point ({idx} of {len(points_to_insert)})")
                try:
                    cursor.insertRow(point)
                except Exception as e:
                    arcpy.AddError(f"Error inserting point: {e}")
                arcpy.SetProgressorPosition()
        return


def as_dict(cursor: arcpy.da.SearchCursor):
    """ Converts a search cursor to a dictionary generator so rows can be accessed by field name """
    for row in cursor:
        yield dict(zip(cursor.fields, row))

 

MordehayAv-revaya
Emerging Contributor

Thank you very much Hayden,  I will check it and update

 

HaydenWelch
Frequent Contributor

I did me up the indentation in the UpdateCursor so you'll have to make sure you fix that.

0 Kudos
BobBooth1
Esri Contributor
0 Kudos