Create Chainage Ticks Along a Line Using ArcPy and ArcGIS Pro

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 createChainageTicksAlongLinesLabel 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 ShortLongFloatDouble, 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)
				
			

Leave a Comment

Your email address will not be published. Required fields are marked *