Feature Vertices to Points (Data Management) on a Basic License with ArcPy

This was a fun one to do! The Feature Vertices to Points (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. Check out the Esri documentation for more information on the tool here. This workflow was created using ArcGIS Pro 3.1.0. You can download the Rivers dataset from the EPA.

Interested in learning ArcPy? check out our course.

The syntax for the Feature Vertices to Points tool is shown below.

arcpy.management.FeatureVerticesToPoints(in_features, out_feature_class, {point_location})

Import the arcpy moudules.

import arcpy

Following the Esri documentation, there are two required parameters; in_features (type: Feature Layer) and out_feature_class (type: Feature Class), and one optional parameter; point_location (type: String). The options for the optional point_location parameter are; ALL (the default), MID, START, END, BOTH_ENDS, DANGLE.

## input fc (linear or polygon)
in_features = arcpy.GetParameterAsText(0)

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

## ALL, MID, START, END, BOTH_ENDS, DANGLE (lines only)
point_location = arcpy.GetParameterAsText(2)

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

## 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 list will hold the details for every point to be added to the
## output feature class for attributes and geometry
point_lst = []

We will create a function to get start, end, and midpoints of a linear feature.

def get_start_end_mid_points(record_atts, geometry, to_return):
    """
    Get the start, end, or mid point of a linear feature and return as a row
    containing the attributes of the original linear feature with a new point
    geometry.

    args:
        record      a row from a GeoDataFrame as a dictionary
        to_return   the point location to return, START, END, MID


    return:
        new_record  a dictionary containing attributes and geometry as keys
    """

    if to_return == "START":
        x, y = geometry.firstPoint.X, geometry.firstPoint.Y
        z, m = geometry.firstPoint.Z, geometry.firstPoint.M
        record_atts.append(arcpy.PointGeometry(arcpy.Point(x, y, z, m)))

    elif to_return == "END":
        x, y = geometry.lastPoint.X, geometry.lastPoint.Y
        z, m = geometry.lastPoint.Z, geometry.lastPoint.M
        record_atts.append(arcpy.PointGeometry(arcpy.Point(x, y, z, m)))

    elif to_return == "MID":
        midpoint = geometry.positionAlongLine(0.50,True)
        record_atts.append(midpoint)

    return record_atts

Next, we get the vertices based on the selection from the point_location parameter.

arcpy.SetProgressorLabel("Getting {0} vertices".format(point_location))

## if the point_location is not ALL, then we will explode multipartt to singlepart
if point_location != "ALL":
    ## explode multipart to singlepart geometry
    single_lines_fc = arcpy.management.MultipartToSinglepart(in_features, "memory\\single")
    in_fld_names[0] = "ORIG_FID"
    if point_location == "DANGLE":
        single_lines_fc = arcpy.management.CalculateGeometryAttributes(single_lines_fc, "DANGLE_LEN LENGTH")

## if the point_location is ALL ################################################

if point_location == "ALL":
    ## interate through each record in the in_features
    with arcpy.da.SearchCursor(in_features, in_fld_names, explode_to_points=True) as cursor:
        ## for each feature/record
        for row in cursor:
            ## append the row into the point_lst as a list
            point_lst.append(list(row))

## if the point location is MID, START, END ####################################

elif point_location in ("MID", "START", "END"):
    ## interate through each record in the in_features
    with arcpy.da.SearchCursor(single_lines_fc, in_fld_names) as cursor:
        ## for each feature/record
        for row in cursor:
            geom = row[-1]
            row_attributes = list(row[0:-1])
            row_attributes = get_start_end_mid_points(row_attributes, geom, point_location)
            point_lst.append(row_attributes)

## if the point location is BOTH_ENDS ##########################################

elif point_location == "BOTH_ENDS":
    ## interate through each record in the in_features
    with arcpy.da.SearchCursor(single_lines_fc, in_fld_names) as cursor:
        ## for each feature/record
        for row in cursor:
            for end in ("START", "END"):
                geom = row[-1]
                row_attributes = list(row[0:-1])
                row_attributes = get_start_end_mid_points(row_attributes, geom, end)
                point_lst.append(row_attributes)

## if the point location is DANGLE #############################################

elif point_location == "DANGLE":
    ## a dangle will only be a start or end point.
    possible_dangles = []

    ## interate through each record in the in_features
    with arcpy.da.SearchCursor(single_lines_fc, "SHAPE@") as cursor:
        ## for each feature/record
        for row in cursor:
            geom = row[0]

            ## get start and end points
            get_start_end_mid_points(possible_dangles, geom, "START")
            get_start_end_mid_points(possible_dangles, geom, "END")

    ## spatial join between points and lines
    sj = arcpy.analysis.SpatialJoin(possible_dangles, single_lines_fc, "memory\\sj", "JOIN_ONE_TO_ONE", "KEEP_ALL",
            match_option="INTERSECT", search_radius="0.01 Meters")

    ## add the field name for DANGLE_LEN
    in_fld_names.append("DANGLE_LEN")

    ## iterate through the spatial join where Join_Count is equal to 1
    ## if count is greater than 1 the point connects to more than one line
    ## and is therefore not a dangle
    with arcpy.da.SearchCursor(sj, in_fld_names, "Join_Count = 1") as cursor:
        for row in cursor:
            ## add list to the point_lst
            point_lst.append(list(row))

    ## delete the sj, it is no longer needed
    arcpy.management.Delete(sj)

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

arcpy.SetProgressorLabel("Creating temporary feature class")

## create a temp point feature class in the memory workspace
temp_fc = arcpy.management.CreateFeatureclass("memory", "temp_vertices", "POINT",
    in_features, "SAME_AS_TEMPLATE", "SAME_AS_TEMPLATE", srs_id)

Next, we alter the schema to add in the ORIG_FID fields as per the Esri documentation for the Feature Vertices to Points tool. The ORIG_FID is only handled here for ALL. ORIG_FID for the rest was taken care of when creating the vertices. For DANGLE, we need to add DANGLE_LEN.

arcpy.SetProgressorLabel("Updating schema")

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

if point_location == "DANGLE":
    ## replace TARTGET_FID
    in_fld_names[-1] = "DANGLE_LEN"
    arcpy.management.AddField(temp_fc, "DANGLE_LEN", field_type="DOUBLE", field_is_nullable="NULLABLE")

if point_location == "ALL":
    ## remove the OID field from in_fld_names
    ## add in the ORIG_FID field name to the in_fld_names list
    in_fld_names[0] = "ORIG_FID"

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

arcpy.SetProgressorLabel("Inserting data")

## inject point record into the output feature class.
with arcpy.da.InsertCursor(temp_fc, in_fld_names) as i_cursor:
    for count, attributes in enumerate(point_lst, 1):
        i_cursor.insertRow(attributes)
        arcpy.SetProgressorPosition()

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

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

And a little bit of house cleaning to finish.

if point_location != "ALL":
    arcpy.management.Delete(single_lines_fc)

arcpy.management.Delete(temp_fc)

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.

Here’s all the code from the above snippets.

import arcpy

################################################################################
## ESRI Documentation:
## https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/feature-vertices-to-points.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/3.0/tool-reference/data-management/multipart-to-singlepart.htm
##  https://pro.arcgis.com/en/pro-app/latest/arcpy/functions/setprogressorlabel.htm
##  https://pro.arcgis.com/en/pro-app/latest/arcpy/data-access/searchcursor-class.htm
##  https://pro.arcgis.com/en/pro-app/latest/arcpy/classes/pointgeometry.htm
##  https://pro.arcgis.com/en/pro-app/latest/arcpy/classes/polyline.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/tool-reference/analysis/spatial-join.htm
##  https://pro.arcgis.com/en/pro-app/latest/help/analysis/geoprocessing/basics/the-in-memory-workspace.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/data-access/insertcursor-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
##
## Syntax:
##     arcpy.management.FeatureVerticesToPoints(in_features, out_feature_class, {point_location})
##
## ArcGIS Pro Version: 3.1.0
##
################################################################################

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

## input fc (linear or polygon)
in_features = arcpy.GetParameterAsText(0)

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

## ALL, MID, START, END, BOTH_ENDS, DANGLE (lines only)
point_location = arcpy.GetParameterAsText(2)

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

## ## 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 list will hold the details for every point to be added to the
## output feature class for attributes and geometry
point_lst = []

################################################################################
## FUNCTIONS ###################################################################

def get_start_end_mid_points(record_atts, geometry, to_return):
    """
    Get the start, end, or mid point of a linear feature and return as a row
    containing the attributes of the original linear feature with a new point
    geometry.

    args:
        record      a row from a GeoDataFrame as a dictionary
        to_return   the point location to return, START, END, MID


    return:
        new_record  a dictionary containing attributes and geometry as keys
    """

    if to_return == "START":
        x, y = geometry.firstPoint.X, geometry.firstPoint.Y
        z, m = geometry.firstPoint.Z, geometry.firstPoint.M
        record_atts.append(arcpy.PointGeometry(arcpy.Point(x, y, z, m)))

    elif to_return == "END":
        x, y = geometry.lastPoint.X, geometry.lastPoint.Y
        z, m = geometry.lastPoint.Z, geometry.lastPoint.M
        record_atts.append(arcpy.PointGeometry(arcpy.Point(x, y, z, m)))

    elif to_return == "MID":
        midpoint = geometry.positionAlongLine(0.50,True)
        record_atts.append(midpoint)

    return record_atts

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

arcpy.SetProgressorLabel("Getting {0} vertices".format(point_location))

## if the point_location is not ALL, then we will explode multipartt to singlepart
if point_location != "ALL":
    ## explode multipart to singlepart geometry
    single_lines_fc = arcpy.management.MultipartToSinglepart(in_features, "memory\\single")
    in_fld_names[0] = "ORIG_FID"
    if point_location == "DANGLE":
        single_lines_fc = arcpy.management.CalculateGeometryAttributes(single_lines_fc, "DANGLE_LEN LENGTH")

## if the point_location is ALL ################################################

if point_location == "ALL":
    ## interate through each record in the in_features
    with arcpy.da.SearchCursor(in_features, in_fld_names, explode_to_points=True) as cursor:
        ## for each feature/record
        for row in cursor:
            ## append the row into the point_lst as a list
            point_lst.append(list(row))

## if the point location is MID, START, END ####################################

elif point_location in ("MID", "START", "END"):
    ## interate through each record in the in_features
    with arcpy.da.SearchCursor(single_lines_fc, in_fld_names) as cursor:
        ## for each feature/record
        for row in cursor:
            geom = row[-1]
            row_attributes = list(row[0:-1])
            row_attributes = get_start_end_mid_points(row_attributes, geom, point_location)
            point_lst.append(row_attributes)

## if the point location is BOTH_ENDS ##########################################

elif point_location == "BOTH_ENDS":
    ## interate through each record in the in_features
    with arcpy.da.SearchCursor(single_lines_fc, in_fld_names) as cursor:
        ## for each feature/record
        for row in cursor:
            for end in ("START", "END"):
                geom = row[-1]
                row_attributes = list(row[0:-1])
                row_attributes = get_start_end_mid_points(row_attributes, geom, end)
                point_lst.append(row_attributes)

## if the point location is DANGLE #############################################

elif point_location == "DANGLE":
    ## a dangle will only be a start or end point.
    possible_dangles = []

    ## interate through each record in the in_features
    with arcpy.da.SearchCursor(single_lines_fc, "SHAPE@") as cursor:
        ## for each feature/record
        for row in cursor:
            geom = row[0]

            ## get start and end points
            get_start_end_mid_points(possible_dangles, geom, "START")
            get_start_end_mid_points(possible_dangles, geom, "END")

    ## spatial join between points and lines
    sj = arcpy.analysis.SpatialJoin(possible_dangles, single_lines_fc, "memory\\sj", "JOIN_ONE_TO_ONE", "KEEP_ALL",
            match_option="INTERSECT", search_radius="0.01 Meters")

    ## add the field name for DANGLE_LEN
    in_fld_names.append("DANGLE_LEN")

    ## iterate through the spatial join where Join_Count is equal to 1
    ## if count is greater than 1 the point connects to more than one line
    ## and is therefore not a dangle
    with arcpy.da.SearchCursor(sj, in_fld_names, "Join_Count = 1") as cursor:
        for row in cursor:
            ## add list to the point_lst
            point_lst.append(list(row))

    ## delete the sj, it is no longer needed
    arcpy.management.Delete(sj)

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

arcpy.SetProgressorLabel("Creating temporary feature class")

## create a temp point feature class in the memory workspace
temp_fc = arcpy.management.CreateFeatureclass("memory", "temp_vertices", "POINT",
    in_features, "SAME_AS_TEMPLATE", "SAME_AS_TEMPLATE", srs_id)

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

arcpy.SetProgressorLabel("Updating schema")

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

if point_location == "DANGLE":
    ## replace TARTGET_FID
    in_fld_names[-1] = "DANGLE_LEN"
    arcpy.management.AddField(temp_fc, "DANGLE_LEN", field_type="DOUBLE", field_is_nullable="NULLABLE")

if point_location == "ALL":
    ## remove the OID field from in_fld_names
    ## add in the ORIG_FID field name to the in_fld_names list
    in_fld_names[0] = "ORIG_FID"

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

arcpy.SetProgressorLabel("Inserting data")

## inject point record into the output feature class.
with arcpy.da.InsertCursor(temp_fc, in_fld_names) as i_cursor:
    for count, attributes in enumerate(point_lst, 1):
        i_cursor.insertRow(attributes)
        arcpy.SetProgressorPosition()

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

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

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

if point_location != "ALL":
    arcpy.management.Delete(single_lines_fc)

arcpy.management.Delete(temp_fc)

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

2 thoughts on “Feature Vertices to Points (Data Management) on a Basic License with ArcPy”

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

  2. Pingback: Geopandas, What a difference a Spatial Index Makes! – finaldraftmapping.com

Leave a Comment

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