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

The Split Line at Vertices (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. While the Advanced tool will take a linear or polygon feature class as input, here, we will only look at splitting linear features. Check out the Esri documentation for more information on the tool here. This workflow was created using ArcGIS Pro 3.1.0.

Interested in learning ArcPy? check out our course.

The syntax for the Split Line at Vertices tool is shown below.

arcpy.management.SplitLine(in_features, out_feature_class)

Let’s get the show on the road with importing arcpy and os modules.

import arcpy
import os

Following the Esri documentation, we require two user inputs; in_features (type: Feature Layer) and out_feature_class (type: Feature Class).

## input feature class (linear)
in_features = arcpy.GetParameterAsText(0)

## output feature class filepath
out_feature_class = arcpy.GetParameterAsText(1)

Next, we will set in place the objects required for our tool. Each requirement is commented for more information.

## the name of the output feature class/shapefile as per user input
out_name = os.path.basename(out_feature_class)

## the workspace the out_feature_class will reside in (one step back from the data itself)
out_workspace = os.path.dirname(out_feature_class)

## srs id of the in_features so we can assign the output the same
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.
vertices_dict = {}

## used for the tool progressor label
feature_count = int(arcpy.GetCount_management(in_features).getOutput(0))

Now we get all vertices per OID and add to our vertices dictionary (vertices_dict variable)

arcpy.SetProgressorLabel("Getting vertices information.")

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

## iterate through each record in the in_features feature class.
## we are only interested in the OID field and the SHAPE@ for accessing geometry.
with arcpy.da.SearchCursor(in_features, [oid_fld, "SHAPE@"]) as cursor:
    ## for each feature/record
    for count, row in enumerate(cursor, 1):
        arcpy.SetProgressorLabel("Processing {0} of {1}".format(count, feature_count))
        ## this list will hold the point geometry for each vertex per OID
        points_list = []
        ## dig into each part of the geometry
        ## row[-1] is the SHAPE@ toke for accessing geometry
        for part in row[-1]:
                ## Step through each vertex in the feature
                for pnt in part:
                    if pnt:
                        ## append the point geometry into the point_list for the OID
                        points_list.append(arcpy.PointGeometry(arcpy.Point(pnt.X, pnt.Y, pnt.Z, pnt.M)))
        ## add the OID as the key in the vertices_dict, and the list of points (vertices) as the dictionary value.
        vertices_dict[row[0]] = points_list

        arcpy.SetProgressorPosition()

arcpy.SetProgressorLabel("Finished gathering vertices information")

Now that we have all our vertices nicely packaged in a dictionary, lets split those lines. Each split line will maintain the attributes from the original line it was split from.

arcpy.SetProgressorLabel("Getting segment information.")

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

## iterate through each linear record from the in_features feature class.
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, feature_count))
        ## get the start point of the line
        first_ln_xy = (ln[-1].firstPoint.X, ln[-1].firstPoint.Y)

        ## select points that belong to the line from the vertices dictionary
        pt_sel = vertices_dict[ln[0]]

        ## this list will hold the 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.firstPoint.X, pt.firstPoint.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 (vertex)
            ## along the line and append the distance to the list.
            else:
                distance = ln[-1].queryPointAndDistance(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
            ## otherwise, lets get the attributes for the line segment.
            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

        arcpy.SetProgressorPosition()

arcpy.SetProgressorLabel("Segment information gathered")
arcpy.ResetProgressor()

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

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

## create a linear feature class in memory using the in_features as a template.
temp_fc = arcpy.CreateFeatureclass_management("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 Vertices tool.

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

## add the ORIG_FID field
arcpy.AddField_management(temp_fc, "ORIG_FID", field_type="LONG", field_is_nullable="NULLABLE")
## add the ORIG_SEQ field
arcpy.AddField_management(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 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)

## use the InsertCursor
with arcpy.da.InsertCursor(temp_fc, in_fld_names) as i_cursor:
    ## for each entry in our segments list
    for attributes in segments_lst:
        ## insert a record
        i_cursor.insertRow(attributes)
        arcpy.SetProgressorPosition()

arcpy.SetProgressorLabel("Finished adding lines")
arcpy.ResetProgressor()

Almost there! Let’s write the data to disk.

arcpy.SetProgressorLabel("Writing output to disk")
arcpy.FeatureClassToFeatureClass_conversion(temp_fc, out_workspace, out_name)

And a little bit of house cleaning to finish.

arcpy.Delete_management(temp_fc)

Save your script, and now that we’ve done the heavy lifting, 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)

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 splitLineAtVertices, Label to Split Line at Vertices (Basic), and the Description to Split Line at Vertices using a Basic License.

In the Parameters tab set as per below. Set the Filter for the Input Features parameter to be Polyline Feature Type.

In the Execution tab, click the folder icon in the top-right corner and ad your saved Python script. Click OK. Run the tool with a linear feature class of your choice (and make sure to check out the neat Tool Progressor information we added throughout).

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

Subscribe to our blog…

All the code

Here’s all the code from the above snippets.

import arcpy
import os

################################################################################
## ESRI Documentation:
## https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/split-line-at-vertices.htm
##
## Syntax:
##     arcpy.management.SplitLine(in_features, out_feature_class)
##
## ArcGIS Pro Version: 3.1.0
##
################################################################################

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

## input feature class (linear)
in_features = arcpy.GetParameterAsText(0)

## output feature class filepath
out_feature_class = arcpy.GetParameterAsText(1)

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

## the name of the output feature class/shapefile as per user input
out_name = os.path.basename(out_feature_class)

## the workspace the out_feature_class will reside in (one step back from the data itself)
out_workspace = os.path.dirname(out_feature_class)

## srs id of the in_features so we can assign the output the same
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.
vertices_dict = {}

## used for the tool progressor label
feature_count = int(arcpy.GetCount_management(in_features).getOutput(0))

################################################################################
## GET VERTICES ################################################################

arcpy.SetProgressorLabel("Getting vertices information.")

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

## iterate through each record in the in_features feature class.
## we are only interested in the OID field and the SHAPE@ for accessing geometry.
with arcpy.da.SearchCursor(in_features, [oid_fld, "SHAPE@"]) as cursor:
    ## for each feature/record
    for count, row in enumerate(cursor, 1):
        arcpy.SetProgressorLabel("Processing {0} of {1}".format(count, feature_count))
        ## this list will hold the point geometry for each vertex per OID
        points_list = []
        ## dig into each part of the geometry
        ## row[-1] is the SHAPE@ toke for accessing geometry
        for part in row[-1]:
                ## Step through each vertex in the feature
                for pnt in part:
                    if pnt:
                        ## append the point geometry into the point_list for the OID
                        points_list.append(arcpy.PointGeometry(arcpy.Point(pnt.X, pnt.Y, pnt.Z, pnt.M)))
        ## add the OID as the key in the vertices_dict, and the list of points (vertices) as the dictionary value.
        vertices_dict[row[0]] = points_list

        arcpy.SetProgressorPosition()

arcpy.SetProgressorLabel("Finished gathering vertices information")

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

arcpy.SetProgressorLabel("Getting segment information.")

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

## iterate through each linear record from the in_features feature class.
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, feature_count))
        ## get the start point of the line
        first_ln_xy = (ln[-1].firstPoint.X, ln[-1].firstPoint.Y)

        ## select points that belong to the line from the vertices dictionary
        pt_sel = vertices_dict[ln[0]]

        ## this list will hold the 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.firstPoint.X, pt.firstPoint.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 (vertex)
            ## along the line and append the distance to the list.
            else:
                distance = ln[-1].queryPointAndDistance(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
            ## otherwise, lets get the attributes for the line segment.
            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

        arcpy.SetProgressorPosition()

arcpy.SetProgressorLabel("Segment information gathered")
arcpy.ResetProgressor()

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

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

## create a linear feature class in memory using the in_features as a template.
temp_fc = arcpy.CreateFeatureclass_management("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.AddField_management(temp_fc, "ORIG_FID", field_type="LONG", field_is_nullable="NULLABLE")
## add the ORIG_SEQ field
arcpy.AddField_management(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)

## use the Insert Cursor to insert one segment at a time as a record
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.FeatureClassToFeatureClass_conversion(temp_fc, out_workspace, out_name)

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

arcpy.Delete_management(temp_fc)

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

4 thoughts on “Split Line at Vertices (Data Management) on a Basic License with ArcPy”

  1. I like these arcpy scripts you are writing. Have you thought about uploading them to github under an open license?

  2. Pingback: Split Line at Point (Data Management) on a Basic License with ArcPy | Final Draft Mapping

  3. Pingback: Split Line at Point (Data Management) on a Basic License with ArcPy – finaldraftmapping.com

Leave a Comment

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