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?
Solved! Go to Solution.
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...
Happy to help if you post up what you have
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()
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...
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!
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))
Thank you very much Hayden, I will check it and update
I did me up the indentation in the UpdateCursor so you'll have to make sure you fix that.
You might find this tutorial helpful:
https://learn.arcgis.com/en/projects/create-a-python-script-tool/