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

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.1.0.

Interested in learning ArcPy? check out our course.

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

arcpy.management.SplitLineAtPoint(in_features, point_features, out_feature_class, {search_radius})

We start with importing the arcpy and os modules.

import arcpy
import os

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)

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 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 = {}

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.GenerateNearTable_analysis(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.GenerateNearTable_analysis(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]))]

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.GetCount_management(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()

We will create a temporary linear 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.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 Point 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)
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()

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)
arcpy.Delete_management(near_tbl)

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

In the Parameters tab set as per below. Set the Filter for in_features and out_feature class to Feature Type: Polyline, and Feature Type: Point for point_features. And don’t miss setting the search_radius as Optional, and also the Directon for out_feature_class to Output.

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 against a point 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.

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-point.htm
##
## Syntax:
##      arcpy.management.SplitLineAtPoint(in_features, point_features, out_feature_class, {search_radius})
##
## ArcGIS Pro Version: 3.1.0
##
################################################################################

################################################################################
## 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 ####################################################

## 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 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.GenerateNearTable_analysis(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.GenerateNearTable_analysis(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.GetCount_management(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.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)
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)
arcpy.Delete_management(near_tbl)

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

Leave a Comment

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