Table of Contents
Introduction
This became a pet project of mine back in 2017 where I first implemented using open source geospatial Python modules, osgeo and shapely. I then converted the workflow to ArcPy before Esri had brought out the Generate Points Along Lines tool. This third iteration of the workflow uses the Generate Points Along Lines tool to reduce the necessary lines of code. You can check out the Esri documentation for the Generate Points Along Lines tool here.
Our goal here is to create transects across linear features at specified distances. Chainage values are often required for linear infrastructure projects. The image below represents an example of the output we want to create.
Leverage ArcPy for geospatial data management workflows within ArcGIS Pro. Learn the fundamentals of utilising ArcGIS Pro geoprocessing tools with ArcPy for data management, conversions, and analysis. This course is packed with amazing content that will help you discover the power of automating tasks within the ArcGIS Pro environment. Take your ArcPy skills from beginner to snake charmer. A little code goes a long way, a little ArcPy provides you with an in-demand skill. Sign up now for our highly rated course.
Custom Tool Syntax
The syntax for the Create Chainage Ticks Along Lines tool is as follows…
createChainageTicksAlongLines(
in_features,
out_feature_class,
distance,
unique_fld,
tick_length
)
We need five parameters, three required; in_features, the linear features to create the chainage ticks for, out_feature_class, the output polyine feature class containing the ticks, and distance, the distance separating each tick along the line, and then we have two optional parameters; unique_fld, is used if you want to assign an attribute from the in_features to the out_feature_class (default is the OID field), and tick_length is for the length of the tick transect, 10 Meters is the default meaning 5 meters either side of the line.
Import the ArcPy module
ArcPy is a Python site package that enables automation, analysis, and management of geographic data within the ArcGIS software environment. Import math from the standard Python library to help with some calculations.
import arcpy
import math
Define the parameters
## input linear feature class
in_features = arcpy.GetParameterAsText(0)
## output feature class containg the ticks
out_feature_class = arcpy.GetParameterAsText(1)
## the distance each tick is to be along the line
distance = arcpy.GetParameterAsText(2)
## a unique identifier to separate line records
unique_fld = arcpy.GetParameterAsText(3)
## length of tick traverse, 50% each side of the line
tick_length = arcpy.GetParameterAsText(4)
Required Python object for the tool
We’ll set some required objects to aid with the workflow. The code is commented, read through and get familiar with each statement.
## required for running the GeneratePointsAlongLines tool
pt_placement = "DISTANCE"
incl_ends = "END_POINTS"
add_chainage_flds = "ADD_CHAINAGE"
## convert to integers
tick_length = int(tick_length)
distance = int(distance.split(" ")[0])
## srs id of the in_features so we can assign the output the same
srs_id = arcpy.Describe(in_features).spatialReference.factoryCode
Required Functions
There are three important functions to create. The first gets the angle between two points, the second gets a point based on the distance and bearing from another point, and the third constructs the chainage tick itself.
## http://wikicode.wikidot.com/get-angle-of-line-between-two-points
def getAngle(pt1, pt2):
"""
Get the angle between two points
Args:
pt1 a tuple containing x,y coordinates
pt2 a tuple containing x,y coordinates
Returns:
A float representing the angle between pt1 and pt2
"""
## pt2 X value - pt1 X value
x_diff = pt2[0] - pt1[0]
## pt2 Y value - pt1 Y value
y_diff = pt2[1] - pt1[1]
return math.degrees(math.atan2(y_diff, x_diff))
## start and end points of chainage tick
## get the first end point of a tick
def getLineEnds(pt, bearing, dist, end_type="END"):
"""
Get the start/end point of the chainage tick
Args:
pt1 a tuple containing x,y coordinates
bearing float representing an angle between two points on the line
dist distance along the bearing where we want to locate the point
Returns:
A tuple representing x,y coords of start/end of chainage line
"""
if end_type == "START":
bearing = bearing + 90
## convert to radians
bearing = math.radians(bearing)
## some mathsy stuff that I dont have a clue about
x = pt[0] + dist * math.cos(bearing)
y = pt[1] + dist * math.sin(bearing)
return (x, y)
## make the chainage tick
def makeTick(chainage_pt, angle_pnt):
## first point on the line and second
angle = getAngle(chainage_pt, angle_pnt)
## get the start point of the chainage tick to create
line_start = getLineEnds(chainage_pt, angle, tick_length/2, "START")
## angle required to construct tick perpendicular to alignment
tick_angle = getAngle(line_start, chainage_pt)
## get end point of tick
line_end = getLineEnds(line_start, tick_angle, tick_length)
## create tick based on start and end point
tick = arcpy.Polyline(arcpy.Array(arcpy.Point(*coords) for coords in [line_start, line_end]))
## return the geometry
return tick
Generate Points Along the Lines
Now we dive into the main workflow by using the Generate Points Along Lines tool. The resulting feature class is held in the memory workspace. The attribute table will hold a field names ORIG_LEN which is the chainage value at each point and and ORIG_FID field which is the FID of the linear feature that the points were generated for.
chainage_points = arcpy.management.GeneratePointsAlongLines(in_features,
"memory\\chainage_pts", pt_placement, distance, "",
incl_ends, add_chainage_flds
)
Creating Linear Feature Class in Memory Workspace
Next, we create a linear feature class in the memory workspace that we will add the tick records to. If the unique_fld is used by the user as input we will add it to the our feature class, if it is not used, we will use ORIG_FID as the field. And of course we need a field to store our chainage values for each tick created.
## in memory feature class to hold the points
temp_fc = arcpy.arcpy.management.CreateFeatureclass("memory", "temp_fc", "POLYLINE", spatial_reference=srs_id)
## if a unique_fld is set add it to the feature class
if unique_fld:
fld_type, fld_length = [[fld.type, fld.length] for fld in arcpy.ListFields(in_features) if fld.name == unique_fld][0]
arcpy.management.AddField(temp_fc, unique_fld, fld_type, field_length=fld_length)
## otherwise add ORIG_FID
else:
arcpy.management.AddField(temp_fc, "ORIG_FID", "LONG")
unique_fld = "ORIG_FID"
## add a field to store the chainage values
arcpy.management.AddField(temp_fc, "chainage", "LONG", field_alias="Chainage")
The Main Event: Create Ticks and Insert into Linear Feature Class
We are at the nerve centre of the workflow now. We get a list of all the FIDs from the original in_features. We then start an insert cursor and iterate through each FID. With each iteration we are adding an entry to a dictionary to represent each point. The key is the chainage value and the value is a tuple containing the X and Y value for the point. We need to iterate through the chainage values sequentially so we create a sorted list of values from the dictionary keys. Iterating through each chainage value we use the information for the current point and the next point to set off our functions and create each chainage tick. We need to account for the last point that it does not have a point after it so we use the point before it
## get a list of FIDs used for the in_features (from the points created)
unique_fids = sorted(set(row[0] for row in arcpy.da.SearchCursor(chainage_points, "ORIG_FID")))
## use an insert cursor
with arcpy.da.InsertCursor(temp_fc, [unique_fld, "chainage", "SHAPE@"]) as cursor:
## for each FID
for fid in unique_fids:
## get all points for the linear feature as a dictionary ORIG_LEN : (x,y)
chainage_dict = {int(row[0]):(row[1].centroid.X, row[1].centroid.Y) for row in arcpy.da.SearchCursor(chainage_points, ["ORIG_LEN", "SHAPE@", "ORIG_FID"]) if row[2]==fid}
## get a sorted list of chainage values
chainage_list = sorted(chainage_dict.keys())
## get the unique value or FID for the chainage tick
unique_value = list(set(row[0] for row in arcpy.da.SearchCursor(chainage_points, [unique_fld, "ORIG_FID"]) if row[1]==fid))[0]
## get the max chainage value as an integer
max_chainage = int(max(chainage_dict.keys()))
## for each chainage value (starting at 0)
for num, chainage in enumerate(chainage_list):
## if the chainage is all but the last one
if num >= 0 and num < len(chainage_list) - 1:
## create the tick line
tick_line = makeTick(chainage_dict[chainage], chainage_dict[chainage_list[num+1]])
## and insert into the memory feature class
cursor.insertRow((unique_value, chainage, tick_line))
## if it is the last chainage
elif num+1 == len(chainage_list):
## we use the point before to create the tick
tick_line = makeTick(chainage_dict[max_chainage], chainage_dict[chainage_list[-2]])
## and insert into the memory feature class
cursor.insertRow((unique_value, max_chainage, tick_line))
Export to disk
Now that the heavy lifting is complete we export the memory linear feature class and save to disk.
arcpy.conversion.ExportFeatures(temp_fc, out_feature_class)
Clean-up memory workspace
Don’t forget your sweeping-brush and clean-up the memory workspace.
arcpy.management.Delete(temp_fc)
arcpy.management.Delete(chainage_points)
Mastering ArcGIS Pro ArcPy Search, Insert, & Update Cursors
Unlock the full potential of ArcPy Cursors with our intensive and in-depth course. Designed for GIS professionals, analysts, and enthusiasts, this course delves into the intricacies of Search, Insert, and Update Cursors in the ArcPy library, empowering you to manipulate and manage spatial data with precision and efficiency.
ArcPy Cursors enable you to streamline your GIS workflows, automate repetitive tasks, and witness the full potential of spatial data manipulation in ArcGIS Pro.
Create the tool in ArcGIS Pro
Save your script and open up ArcGIS Pro. Right-click on your toolbox/toolset of choice and select New > Script. The New Script window will appear. In the General tab set Name to createChainageTicksAlongLines, Label to Create Chainage Ticks Along Lines, and the Description to Create chainage ticks along lines at a user specified distance.
In the Parameters tab set as per below. Set the Filter for the Input Features parameter to Polyline. Also set the Filter for the Output Feature Class parameter to Polyline and the Direction to Output. For the Distance parameter set the Type to Optional and the Default to 10 METERS. For the Name Field parameter set the Type to Optional and the Dependency to in_features, set the Filter to Short, Long, Float, Double, and Text fields only. And lastly, for the Tick Length parameter, set the Type to Optional and the Default to 10.
In the Execution tab, click the folder icon in the top-right corner and add your saved Python script.
Click OK and you are ready to take your new tool for a spin.
You can download the tool and other custom tools over on this page. This tool is in the Custom Tools on a Basic License with ArcPy section.
All the code in one place
You can find the entire code workflow below with links to important components in the documentation that were used.
import arcpy
import math
################################################################################
## Esri Documentation
## https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/generate-points-along-lines.htm
## https://pro.arcgis.com/en/pro-app/latest/arcpy/functions/getparameterastext.htm
## https://pro.arcgis.com/en/pro-app/latest/arcpy/functions/describe.htm
## https://pro.arcgis.com/en/pro-app/latest/arcpy/functions/listfields.htm
## https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/add-field.htm
## https://pro.arcgis.com/en/pro-app/latest/arcpy/classes/polyline.htm
## https://pro.arcgis.com/en/pro-app/latest/arcpy/classes/array.htm
## https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/create-feature-class.htm
## https://pro.arcgis.com/en/pro-app/latest/arcpy/data-access/searchcursor-class.htm
## https://pro.arcgis.com/en/pro-app/latest/arcpy/data-access/updatecursor-class.htm
## https://pro.arcgis.com/en/pro-app/latest/tool-reference/conversion/export-features.htm
## https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/delete.htm
##
## ArcGIS Pro Version 3.1.0
##
#################################################################################
#################################################################################
## USER INPUTS
## input linear feature class
in_features = arcpy.GetParameterAsText(0)
## output feature class containg the ticks
out_feature_class = arcpy.GetParameterAsText(1)
## the distance each tick is to be along the line
distance = arcpy.GetParameterAsText(2)
## a unique identifier to separate line records
unique_fld = arcpy.GetParameterAsText(3)
## legth of tick traverse, 50% each side of the line
tick_length = arcpy.GetParameterAsText(4)
################################################################################
## REQUIRED OBJECTS
## required for running the GeneratePointsAlongLines tool
pt_placement = "DISTANCE"
incl_ends = "END_POINTS"
add_chainage_flds = "ADD_CHAINAGE"
## convert to integers
tick_length = int(tick_length)
distance = int(distance.split(" ")[0])
## srs id of the in_features so we can assign the output the same
srs_id = arcpy.Describe(in_features).spatialReference.factoryCode
################################################################################
## FUNCTIONS ###################################################################
## http://wikicode.wikidot.com/get-angle-of-line-between-two-points
def getAngle(pt1, pt2):
"""
Get the angle between two points
Args:
pt1 a tuple containing x,y coordinates
pt2 a tuple containing x,y coordinates
Returns:
A float representing the angle between pt1 and pt2
"""
## pt2 X value - pt1 X value
x_diff = pt2[0] - pt1[0]
## pt2 Y value - pt1 Y value
y_diff = pt2[1] - pt1[1]
return math.degrees(math.atan2(y_diff, x_diff))
## start and end points of chainage tick
## get the first end point of a tick
def getLineEnds(pt, bearing, dist, end_type="END"):
"""
Get the start/end point of the chainage tick
Args:
pt1 a tuple containing x,y coordinates
bearing float representing an angle between two points on the line
dist distance along the bearing where we want to locate the point
Returns:
A tuple representing x,y coords of start/end of chainage line
"""
if end_type == "START":
bearing = bearing + 90
## convert to radians
bearing = math.radians(bearing)
## some mathsy stuff that I dont have a clue about
x = pt[0] + dist * math.cos(bearing)
y = pt[1] + dist * math.sin(bearing)
return (x, y)
## make the chainage tick
def makeTick(chainage_pt, angle_pnt):
## first point on the line and second
angle = getAngle(chainage_pt, angle_pnt)
## get the start point of the chainage tick to create
line_start = getLineEnds(chainage_pt, angle, tick_length/2, "START")
## angle required to construct tick perpendicular to alignment
tick_angle = getAngle(line_start, chainage_pt)
## get end point of tick
line_end = getLineEnds(line_start, tick_angle, tick_length)
## create tick based on start and end point
tick = arcpy.Polyline(arcpy.Array(arcpy.Point(*coords) for coords in [line_start, line_end]))
## return the geometry
return tick
################################################################################
## GENERATE POINTS ALONG LINES #################################################
chainage_points = arcpy.management.GeneratePointsAlongLines(in_features,
"memory\\chainage_pts", pt_placement, distance, "",
incl_ends, add_chainage_flds
)
################################################################################
## CREATE TEMP POLYLINE FEATURE CLASS IN MEMORY ################################
## in memory feature class to hold the points
temp_fc = arcpy.arcpy.management.CreateFeatureclass("memory", "temp_fc", "POLYLINE", spatial_reference=srs_id)
## if a unique_fld is set add it to the feature class
if unique_fld:
fld_type, fld_length = [[fld.type, fld.length] for fld in arcpy.ListFields(in_features) if fld.name == unique_fld][0]
arcpy.management.AddField(temp_fc, unique_fld, fld_type, field_length=fld_length)
## otherwise add ORIG_FID
else:
arcpy.management.AddField(temp_fc, "ORIG_FID", "LONG")
unique_fld = "ORIG_FID"
## add a field to store the chainage values
arcpy.management.AddField(temp_fc, "chainage", "LONG", field_alias="Chainage")
################################################################################
## GENERATE TICKS ##############################################################
## get a list of FIDs used for the in_features (from the points created)
unique_fids = sorted(set(row[0] for row in arcpy.da.SearchCursor(chainage_points, "ORIG_FID")))
## use an insert cursor
with arcpy.da.InsertCursor(temp_fc, [unique_fld, "chainage", "SHAPE@"]) as cursor:
## for each FID
for fid in unique_fids:
## get all points for the linear feature as a dictionary ORIG_LEN : (x,y)
chainage_dict = {int(row[0]):(row[1].centroid.X, row[1].centroid.Y) for row in arcpy.da.SearchCursor(chainage_points, ["ORIG_LEN", "SHAPE@", "ORIG_FID"]) if row[2]==fid}
## get a sorted list of chainage values
chainage_list = sorted(chainage_dict.keys())
## get the unique value or FID for the chainage tick
unique_value = list(set(row[0] for row in arcpy.da.SearchCursor(chainage_points, [unique_fld, "ORIG_FID"]) if row[1]==fid))[0]
## get the max chainage value as an integer
max_chainage = int(max(chainage_dict.keys()))
## for each chainage value (starting at 0)
for num, chainage in enumerate(chainage_list):
## if the chainage is all but the last one
if num >= 0 and num < len(chainage_list) - 1:
## create the tick line
tick_line = makeTick(chainage_dict[chainage], chainage_dict[chainage_list[num+1]])
## and insert into the memory feature class
cursor.insertRow((unique_value, chainage, tick_line))
## if it is the last chainage
elif num+1 == len(chainage_list):
## we use the point before to create the tick
tick_line = makeTick(chainage_dict[max_chainage], chainage_dict[chainage_list[-2]])
## and insert into the memory feature class
cursor.insertRow((unique_value, max_chainage, tick_line))
################################################################################
## SAVE TO DISK ################################################################
arcpy.conversion.ExportFeatures(temp_fc, out_feature_class)
################################################################################
## CLEAN-UP MEMORY WORKSPACE ###################################################
arcpy.management.Delete(temp_fc)
arcpy.management.Delete(chainage_points)