Table of Contents
Introduction
This was a fun and challenging 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.2.2.
ArcPy for Data Management and Geoprocessing with ArcGIS Pro
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
in_features
and out_feature_class
, and one optional parameter; point_location
.
arcpy.management.FeatureVerticesToPoints(
in_features,
out_feature_class,
{point_location}
)
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, 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)
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 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 = []
Helper function for getting the start, end, and midpoints of lines
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
The Main Event: Getting vertices based on user choice
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)
Create a temporary feature class and define schema
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"
Add the vertices information to the temporary feature class
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()
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)
if point_location != "ALL":
arcpy.management.Delete(single_lines_fc)
arcpy.management.Delete(temp_fc)
Mastering ArcGIS Pro ArcPy Search, Insert, & Update Cursors
A comprehensive course that covers ArcPy cursors in exquisite detail. Unlock the full potential of ArcPy Cursors with an 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. This course is accredited by the Association for Geographic Information (AGI)
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.
![](https://learn.finaldraftmapping.com/wp-content/uploads/2024/03/fdmga_blog_splitlines_1.png)
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 featureVerticesToPoints, Label to Feature Vertices to Points (Basic), and the Description to Feature Vertices to Points using a Basic License.
![](https://learn.finaldraftmapping.com/wp-content/uploads/2024/03/fdmga_blog_fvtp_1-1024x503.png)
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.
![](https://learn.finaldraftmapping.com/wp-content/uploads/2024/03/fdmga_blog_fvtp_2-1024x429.png)
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/3.2/tool-reference/data-management/feature-vertices-to-points.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/data-management/multipart-to-singlepart.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/polyline.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/tool-reference/analysis/spatial-join.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.FeatureVerticesToPoints(in_features, out_feature_class, {point_location})
##
## ArcGIS Pro Version: 3.2.2
##
################################################################################
################################################################################
## 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)
################################################################################