Split Line at Point (Data Management) on a Basic License with ArcPy

Table of Contents

Introduction

This one re-uses plenty of lines of code from the Split Line at Vertices tool. The Split Line at Point (Data Management) geoprocessing tool is only available in ArcGIS Pro with an Advanced license. Here’s how you can use ArcPy to achieve a similar output (if not the exact same for this one!). Check out the Esri documentation for more information on the tool here. This workflow was created using ArcGIS Pro 3.2.2.

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.

Syntax

The syntax for the Split Line at Point tool is as follows with three required parameters; in_features, point_features, and out_feature_class, and one optional parameter; search_radius.
				
					arcpy.management.SplitLineAtPoint(
    in_features,
    point_features,
    out_feature_class,
    {search_radius}
)
				
			

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 arcpy
				
			

Define the parameters

Following the Esri documentation, we require three required user inputs; in_features (type: Feature Layer), point_features (type: Feature Layer) and out_feature_class (type: Feature Class), along with one optional parameter; search_radius (type: Linear Unit).

				
					## Linear feature class to split
in_features = arcpy.GetParameterAsText(0)
 
## the point feature class to split the lines by
point_features = arcpy.GetParameterAsText(1)
 
## output workspace; gdb or folder for shp
out_feature_class = arcpy.GetParameterAsText(2)
 
## snap points within distance to closest point on the line
search_radius = arcpy.GetParameterAsText(3)
				
			

Required Python objects for the tool

We require a handful of Python objects to aid with the workflow. Each of these are commented in the code below.

				
					## srs id of the in_features to assign the same to the output
srs_id = arcpy.Describe(in_features).spatialReference.factoryCode
 
## this list will hold the names of the input fields in order
in_fld_names = [fld.name for fld in arcpy.ListFields(in_features) if fld.type not in ("Blob","Geometry","GlobalID","Guid","OID","Raster")]
 
## add field for accessing geometry
in_fld_names.append("SHAPE@")
 
## we need the OID field for the ORIG_ID output_field
oid_fld = [fld.name for fld in arcpy.ListFields(in_features) if fld.type=="OID"][0]
in_fld_names.insert(0, oid_fld)
 
## this will hold information for all line segments to create
## it will be a list of lists containing attributes and geometry
segments_lst = []
 
## dictionary to hold the key: OID, value: vertice points for each line.
points_dict = {}
				
			

Generate Near Table and get points associated with each line

Now, we use the Generate Near Table tool to get the points that are relevant for splitting the lines. We add the list of points associated with each line to the points_dict where the OID of the line is the key, and the geometry of each point represents where the line is to be split.

				
					## if no search_radius set we are interested in only one point, that is the point
## that is closest to the line
if not search_radius:
    near_tbl = arcpy.analysis.GenerateNearTable(in_features, point_features, "memory\\near_tbl",
    "", "LOCATION", closest="CLOSEST")

## otherwise we want all points found within the search_radius for each line.
else:
    near_tbl = arcpy.analysis.GenerateNearTable(in_features, point_features, "memory\\near_tbl",
    search_radius, "LOCATION", closest="ALL")

## for each near table entry
with arcpy.da.SearchCursor(near_tbl, ["IN_FID", "FROM_X", "FROM_Y"]) as cursor:
    for row in cursor:
        ## if the IN_FID (linear) not in the dictionary add it with a list
        if row[0] not in points_dict:
            points_dict[row[0]] = [arcpy.PointGeometry(arcpy.Point(row[1], row[2]))]
        ## if it is in the dictionary add to the list of points asscoiated to the line
        else:
            points_dict[row[0]] = points_dict[row[0]] + [arcpy.PointGeometry(arcpy.Point(row[1], row[2]))]
				
			

The Main Event: Split each line at the relevant point(s)

Where all the magic happens! Now that we have our points to split each line we go ahead and do just that! We need to take into account that not all linear features have a point associated with it to be split. For example, points closest to the line may fall outside the search radius. This means we would get a KeyError when accessing out points_dict where the key is the OID of the line. In this case, the full line is to be included in the export. The code below prepares the data to be added to the output feature class.

				
					arcpy.SetProgressorLabel("Getting segment information.")

line_count = int(arcpy.management.GetCount(in_features).getOutput(0))

arcpy.SetProgressor("step", "{0} linear features found".format(line_count), 0, line_count, 1)

## search through each linear record
with arcpy.da.SearchCursor(in_features, in_fld_names) as ln_cursor:
    for count, ln in enumerate(ln_cursor, 1):
        arcpy.SetProgressorLabel("Processing {0} of {1} lines".format(count, line_count))
        ## get the start point of the line
        first_ln_xy = (ln[-1].firstPoint.X, ln[-1].firstPoint.Y)

        ## some lines may not have a closest point
        try:
            pt_sel = points_dict[ln[0]]

            ## will hold a list of distances points are along a line to help
            ## chop up a line in order
            distances = []

            ## for each PointGeometry in the list
            for pt in pt_sel:
                ## get the X,Y of the point
                pt_xy = (pt.centroid.X, pt.centroid.Y)

                ## if the point is a start; do nothing
                if  pt_xy == first_ln_xy:
                    pass

                ## otherwise, lets get the distance along the line for each point
                ## and append the distance to the list.
                else:
                    distance = ln[-1].queryPointAndDistance(pt)[1]
                    distances.append(distance)

            ## acount that we need the last segment to the end point
            end_pt = arcpy.PointGeometry(arcpy.Point(ln[-1].lastPoint.X, ln[-1].lastPoint.Y))
            distance = ln[-1].queryPointAndDistance(end_pt)[1]
            distances.append(distance)

            ## 0.0 is the start of the line
            start_dist = 0.0

            ## sequence number per split segment of each line
            ## the sequence number is added to the output as per Esri documentation
            seq_num = 1

            ## iterate through a sorted list of distances
            ## and cut the line at each distance from the start distance
            ## the start distance becomes the current distance for each iteration
            for distance in sorted(distances):
                segment = ln[-1].segmentAlongLine(start_dist, distance)
                ## if segment has no length move on to the next line, we have hit the end
                if segment.getLength('PLANAR', 'METERS') == 0.0:
                    continue
                else:
                    start_dist = distance
                    ## the attributes from the original line
                    segment_attributes = list(ln[0:-1])
                    ## the segment geometry
                    segment_attributes.append(segment)
                    ## the sequence number of the segment for this line
                    segment_attributes.append(seq_num)
                    ## append the info above into our segments list
                    segments_lst.append(segment_attributes)
                    ## increase the sequence number for the next segment
                    seq_num += 1

        ## handle with a KeyError rexception
        except KeyError:
            ## get the attributes/shape for the entire line
            segment_attributes = list(ln)
            ## it will have an ORIG_SEQ of 1
            segment_attributes.append(1)
            ## apend the information to the segments list.
            segments_lst.append(segment_attributes)

        arcpy.SetProgressorPosition()

arcpy.SetProgressorLabel("Finalising output feature class")
arcpy.ResetProgressor()
				
			

Create a temporary feature class and define schema

We will create a temporary point feature class in the memory workspace.

				
					arcpy.SetProgressorLabel("Creating output feature class.")

## create a linear feature class based from a template of teh original input
temp_fc = arcpy.management.CreateFeatureclass("memory", "temp_lines", "POLYLINE",
    in_features, "SAME_AS_TEMPLATE", "SAME_AS_TEMPLATE", srs_id)
				
			

Next, we alter the schema to add in the ORIG_FID and ORIG_SEQ fields as per the Esri documentation for the Split Line at Point tool.

				
					arcpy.SetProgressorLabel("Adding fields to feature class.")

## add the ORIG_FID field
arcpy.management.AddField(temp_fc, "ORIG_FID", field_type="LONG", field_is_nullable="NULLABLE")
## add the ORIG_SEQ field
arcpy.management.AddField(temp_fc, "ORIG_SEQ", field_type="LONG", field_is_nullable="NULLABLE")

## adjust our in_fld_names list to account for the added fields.
## and remove the OID field.
in_fld_names.remove(oid_fld)
in_fld_names.insert(0, "ORIG_FID")
in_fld_names.append("ORIG_SEQ")
				
			

Add the linear information to the temporary feature class

Insert the data into our linear feature class that is held in the memory workspace.

				
					arcpy.SetProgressorLabel("Adding split lines to the output feature class.")
 
total_lines = len(segments_lst)
 
arcpy.SetProgressor("step", "{0} lines to add".format(total_lines), 0, total_lines, 1)
with arcpy.da.InsertCursor(temp_fc, in_fld_names) as i_cursor:
    for attributes in segments_lst:
        i_cursor.insertRow(attributes)
        arcpy.SetProgressorPosition()
arcpy.SetProgressorLabel("Finished adding lines")
arcpy.ResetProgressor()
				
			

Write the output to disk and clean-up

Almost there! Let’s write the data to disk and a little bit of house cleaning to finish.

				
					arcpy.SetProgressorLabel("Writing output to disk")
arcpy.conversion.ExportFeatures(temp_fc, out_feature_class)

arcpy.management.Delete(temp_fc)
arcpy.management.Delete(near_tbl)
				
			

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 let’s head over to ArcGIS Pro and add our tool to a Toolbox. I’ve created a Toolbox (.atbx) in the aptly named folder called Toolbox, and named the Toolbox Advanced_Tools_with_Basic_License.atbx by simply right-clicking on the Toolbox folder and selecting New > Toolbox (.atbx). You can create the tool in a toolbox of your choice.

Right-click on the Advanced_Tools_with_Basic_License.atbx and select New > Script. The New Script window will appear. In the General tab set Name to featureVerticesToPointsLabel to Feature Vertices to Points (Basic), and the Description to Feature Vertices to Points using a Basic License.

In the Parameters tab set as per below. Set the Filter for the Input Features parameter to be Polyline or Polygon Feature Type. Set the Filter for Point Type to a list of strings; ALL, MID, START, END, BOTH_ENDS. And set the Default for Point Type to ALL, and the Direction to Output.

In the Execution tab, click the folder icon in the top-right corner and add your saved Python script.

Open Validation tab and set the updateMessage() as per below. This will prevent DANGLE being used with a plygon.

				
					def updateMessages(self):
    # Customize messages for the parameters.
    # This gets called after standard validation.
    fc = self.params[0].value
    selection = self.params[2].value
    shape_type = arcpy.Describe(fc).shapeType
    if shape_type == "Polygon" and selection == "DANGLE":
        self.params[0].setErrorMessage("Cannot use Polygon with DANGLE")
    elif shape_type == "Polyline":
        self.params[0].clearMessage()
    return
				
			

Click OK. Run the tool with a linear or polygon feature class of your choice.

You can download the tool and other Advanced tools with a Basic license over on this page.

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

################################################################################
## Esri Documentation:
##  https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/split-line-at-point.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/functions/getparameterastext.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/functions/describe.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/functions/listfields.htm
##  https://pro.arcgis.com/en/pro-app/3.2/tool-reference/analysis/generate-near-table.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/functions/setprogressorlabel.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/data-access/searchcursor-class.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/classes/pointgeometry.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/classes/point.htm
##  https://pro.arcgis.com/en/pro-app/3.2/tool-reference/data-management/create-feature-class.htm
##  https://pro.arcgis.com/en/pro-app/3.2/help/analysis/geoprocessing/basics/the-in-memory-workspace.htm
##  https://pro.arcgis.com/en/pro-app/3.2/tool-reference/data-management/add-field.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/data-access/insertcursor-class.htm
##  https://pro.arcgis.com/en/pro-app/3.2/tool-reference/conversion/export-features.htm
##  https://pro.arcgis.com/en/pro-app/3.2/tool-reference/data-management/delete.htm
##
## Syntax:
##      arcpy.management.SplitLineAtPoint(in_features, point_features, out_feature_class, {search_radius})
##
## ArcGIS Pro Version: 3.2.2
##
################################################################################

################################################################################
## USER INPUTS #################################################################

## Linear feature class to split
in_features = arcpy.GetParameterAsText(0)

## the point feature class to split the lines by
point_features = arcpy.GetParameterAsText(1)

## output workspace; gdb or folder for shp
out_feature_class = arcpy.GetParameterAsText(2)

## snap points within distance to closest point on the line
search_radius = arcpy.GetParameterAsText(3)

################################################################################
## TOOL OBJECT REQUIREMENTS ####################################################

## srs id of the in_features to assign the same to the output
srs_id = arcpy.Describe(in_features).spatialReference.factoryCode

## this list will hold the names of the input fields in order
in_fld_names = [fld.name for fld in arcpy.ListFields(in_features) if fld.type not in ("Blob","Geometry","GlobalID","Guid","OID","Raster")]

## add field for accessing geometry
in_fld_names.append("SHAPE@")

## we need the OID field for the ORIG_ID output_field
oid_fld = [fld.name for fld in arcpy.ListFields(in_features) if fld.type=="OID"][0]
in_fld_names.insert(0, oid_fld)

## this will hold information for all line segments to create
## it will be a list of lists containing attributes and geometry
segments_lst = []

## dictionary to hold the key: OID, value: vertice points for each line.
points_dict = {}

################################################################################
## NEAR TABLE ##################################################################

## if no search_radius set we are interested in only one point, that is the point
## that is closest to the line
if not search_radius:
    near_tbl = arcpy.analysis.GenerateNearTable(in_features, point_features, "memory\\near_tbl",
    "", "LOCATION", closest="CLOSEST")

## otherwise we want all points found within the search_radius for each line.
else:
    near_tbl = arcpy.analysis.GenerateNearTable(in_features, point_features, "memory\\near_tbl",
    search_radius, "LOCATION", closest="ALL")

## for each near table entry
with arcpy.da.SearchCursor(near_tbl, ["IN_FID", "FROM_X", "FROM_Y"]) as cursor:
    for row in cursor:
        ## if the IN_FID (linear) not in the dictionary add it with a list
        if row[0] not in points_dict:
            points_dict[row[0]] = [arcpy.PointGeometry(arcpy.Point(row[1], row[2]))]
        ## if it is in the dictionary add to the list of points asscoiated to the line
        else:
            points_dict[row[0]] = points_dict[row[0]] + [arcpy.PointGeometry(arcpy.Point(row[1], row[2]))]

################################################################################
## SPLIT LINES #################################################################

arcpy.SetProgressorLabel("Getting segment information.")

line_count = int(arcpy.management.GetCount(in_features).getOutput(0))

arcpy.SetProgressor("step", "{0} linear features found".format(line_count), 0, line_count, 1)

## search through each linear record
with arcpy.da.SearchCursor(in_features, in_fld_names) as ln_cursor:
    for count, ln in enumerate(ln_cursor, 1):
        arcpy.SetProgressorLabel("Processing {0} of {1} lines".format(count, line_count))
        ## get the start point of the line
        first_ln_xy = (ln[-1].firstPoint.X, ln[-1].firstPoint.Y)

        ## some lines may not have a closest point
        try:
            pt_sel = points_dict[ln[0]]

            ## will hold a list of distances points are along a line to help
            ## chop up a line in order
            distances = []

            ## for each PointGeometry in the list
            for pt in pt_sel:
                ## get the X,Y of the point
                pt_xy = (pt.centroid.X, pt.centroid.Y)

                ## if the point is a start; do nothing
                if  pt_xy == first_ln_xy:
                    pass

                ## otherwise, lets get the distance along the line for each point
                ## and append the distance to the list.
                else:
                    distance = ln[-1].queryPointAndDistance(pt)[1]
                    distances.append(distance)

            ## acount that we need the last segment to the end point
            end_pt = arcpy.PointGeometry(arcpy.Point(ln[-1].lastPoint.X, ln[-1].lastPoint.Y))
            distance = ln[-1].queryPointAndDistance(end_pt)[1]
            distances.append(distance)

            ## 0.0 is the start of the line
            start_dist = 0.0

            ## sequence number per split segment of each line
            ## the sequence number is added to the output as per Esri documentation
            seq_num = 1

            ## iterate through a sorted list of distances
            ## and cut the line at each distance from the start distance
            ## the start distance becomes the current distance for each iteration
            for distance in sorted(distances):
                segment = ln[-1].segmentAlongLine(start_dist, distance)
                ## if segment has no length move on to the next line, we have hit the end
                if segment.getLength('PLANAR', 'METERS') == 0.0:
                    continue
                else:
                    start_dist = distance
                    ## the attributes from the original line
                    segment_attributes = list(ln[0:-1])
                    ## the segment geometry
                    segment_attributes.append(segment)
                    ## the sequence number of the segment for this line
                    segment_attributes.append(seq_num)
                    ## append the info above into our segments list
                    segments_lst.append(segment_attributes)
                    ## increase the sequence number for the next segment
                    seq_num += 1

        ## handle with a KeyError rexception
        except KeyError:
            ## get the attributes/shape for the entire line
            segment_attributes = list(ln)
            ## it will have an ORIG_SEQ of 1
            segment_attributes.append(1)
            ## apend the information to the segments list.
            segments_lst.append(segment_attributes)

        arcpy.SetProgressorPosition()

arcpy.SetProgressorLabel("Finalising output feature class")
arcpy.ResetProgressor()

################################################################################
## CREATE OUPUT FEATURE CLASS ##################################################

arcpy.SetProgressorLabel("Creating output feature class.")

## create a linear feature class based from a template of teh original input
temp_fc = arcpy.management.CreateFeatureclass("memory", "temp_lines", "POLYLINE",
    in_features, "SAME_AS_TEMPLATE", "SAME_AS_TEMPLATE", srs_id)

################################################################################
## CREATE OUPUT FEATURE CLASS SCHEMA ###########################################

arcpy.SetProgressorLabel("Adding fields to feature class.")

## add the ORIG_FID field
arcpy.management.AddField(temp_fc, "ORIG_FID", field_type="LONG", field_is_nullable="NULLABLE")
## add the ORIG_SEQ field
arcpy.management.AddField(temp_fc, "ORIG_SEQ", field_type="LONG", field_is_nullable="NULLABLE")

## adjust our in_fld_names list to account for the added fields.
## and remove the OID field.
in_fld_names.remove(oid_fld)
in_fld_names.insert(0, "ORIG_FID")
in_fld_names.append("ORIG_SEQ")

################################################################################
## INSERT THE DATA #############################################################

arcpy.SetProgressorLabel("Adding split lines to the output feature class.")

total_lines = len(segments_lst)

arcpy.SetProgressor("step", "{0} lines to add".format(total_lines), 0, total_lines, 1)
with arcpy.da.InsertCursor(temp_fc, in_fld_names) as i_cursor:
    for attributes in segments_lst:
        i_cursor.insertRow(attributes)
        arcpy.SetProgressorPosition()
arcpy.SetProgressorLabel("Finished adding lines")
arcpy.ResetProgressor()

################################################################################
## WRITE TO DISK ###############################################################

arcpy.SetProgressorLabel("Writing output to disk")
arcpy.conversion.ExportFeatures(temp_fc, out_feature_class)

################################################################################
## CLEAN UP ####################################################################

arcpy.management.Delete(temp_fc)
arcpy.management.Delete(near_tbl)

################################################################################
				
			

Leave a Comment

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