Table of Contents
Introduction
Not the most fanciest of tools but another one none-the-less that you can by-pass the Advanced license requirement and create your own tool using ArcPy. The Feature Envelope To Polygon (Data Management) is only available in ArcGIS Pro with an Advanced license. You can achieve quite similar to this tool using the Minimum Bounding Geometry (Data Management) that is available with a Basic license. Check out the Esri documentation for the Feature Envelope To Polygon tool here, and the Minimum Bounding Geometry tool here. With the latter you cannot choose to build envelopes per multipart like you can in the former so we will account for that in our custom tool as we use ArcPy to overcome the Advanced license barrier. This workflow was created using ArcGIS Pro 3.1.0.
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 Feature Envelope to Polygon tool is shown below.
arcpy.management.FeatureEnvelopeToPolygon(
in_features,
out_feature_class, {single_envelope}
)
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 two required user inputs; in_features (type: Feature Layer) and out_feature_class (type: Feature Class), and one optional parameter; single_envelope (type: Boolean – although we’ll use a String).
## the input features that can be multipoint, line, polygon
in_features = arcpy.GetParameterAsText(0)
## the output polygon feature class
out_feature_class = arcpy.GetParameterAsText(1)
## one envelope for entire multipart of sperate envelope for each part of the
## multipart.
single_envelope = arcpy.GetParameterAsText(2)
Get the Shape Type
Knowing the feature class shape type is important for the workflow, we need to handle for Multipoint, Polygon, and Polyline. The in_features parameter will be limited in the user input when we create our tool in ArcGIS Pro to these three shape types.
## get the shape type of the in_features feature class
shape_type = arcpy.Describe(in_features).shapeType
Singlepart and Multipart Sections
Next up is the easy part. If the shape type is Polygon or Polyline and SINGLEPART is selected, OR the shape type is Multipoint and MULTIPART is selected, we can simply use the Minimum Bounding Geometry tool to achieve our desired output. We will place a validation that an incorrect selection of Multipoint and SINGLEPART cannot be entered when using our tools. This will prevent running the tool with a Multipoint feature class and a SINGLEPART selection.
## Validation will only allow SINGLEPART to be chosen for Multipoint
if single_envelope == "SINGLEPART" or (shape_type == "Multipoint" and single_envelope == "MULTIPART"):
## we can simply use the MBG tool available for a Basic License to achieve
## desired output
arcpy.management.MinimumBoundingGeometry(in_features, out_feature_class, "ENVELOPE")
If it is a Multipart Polygon/Polyline and Required Python Objects
If the in_features have a shape type of Polygon or Polyline and a selection single_envelope set to MULTIPART things get a little trickier and we’re going to use plenty of ArcPy tools and objects to achieve our goal. As mentioned in the previous paragraph, the Feature Envelope to Polygon tool does not allow you to run using Multipoint geometry and SINGLEPART selected. First, we need to get some information so we can recreate the attribute table in the output.
## if MULTIPART was selection for a Polygon or Polyline Feature Class
elif single_envelope == "MULTIPART" and shape_type in ("Polygon", "Polyline"):
## we need the OID field to aid will matching original records with output records
oid_fld = [fld.name for fld in arcpy.ListFields(in_features) if fld.type=="OID"][0]
## get a list of fields required for the output
in_fld_names = [fld.name for fld in arcpy.ListFields(in_features) if fld.type not in ("Blob","Geometry","GlobalID","Guid","OID","Raster")]
## at the OID field as the first field name in the list
in_fld_names.insert(0, oid_fld)
## get the SRS of the in_features feature class
srs_id = arcpy.Describe(in_features).spatialReference.factoryCode
## dictionary to hold {oid:geometry} this will hold all the geometry we need to
## reapply to our memory_fc
poly_dict = {}
Create a temporary feature class and define schema
Next, we create a Polygon feature class in the memory workspace for our output. This attribute table is a template of the in_features and we add an ORIG_OID field to help match up the original in_features to records in the memory feature class. We populate the attribute table of our memory feature class with attributes from the in_features.
## create a Polygon feature class in the memory workspace
memory_fc = arcpy.management.CreateFeatureclass("memory", "memory_fc", "POLYGON", in_features,
"SAME_AS_TEMPLATE", "SAME_AS_TEMPLATE", srs_id)
## add the ORIG_OID field
arcpy.management.AddField(memory_fc, "ORIG_OID", "LONG")
## iterate through the in_features feature class and populate the
## attribute table for feature class in memorry
with arcpy.da.SearchCursor(in_features, in_fld_names) as cursor:
in_fld_names[0] = "ORIG_OID"
for row in cursor:
with arcpy.da.InsertCursor(memory_fc, in_fld_names) as i_cursor:
i_cursor.insertRow(row)
Multipart to Singlepart
Next, we use the out-of-the-box Multipart To Singlepart tool (Esri documentation here). This will explode all Multipart geometries to Singlepart geometry. This tool adds an ORIG_FID field to the output and so does the Minimum Bounding Geometry tool coming up shortly. To keep it explicit and to avoid clashes and tool confusion we add in a MP_HELPER (MP = Multipart) field to maintain the ORIG_FID for us and use the Calculate Field tool to populate the field.
## Step 1. create singlepart from multipart
singlepart = arcpy.management.MultipartToSinglepart(in_features, "memory\\single_part")
## add in a field to maintain the ORIG_FID field info generated from the
## MultipartToSinglepart tool. The MBG tool also uses ORIG_FID and it can
## cause confusion so we will be explicit and populate out own MP_HELPER field
## with the ORIG_FID
arcpy.management.AddField(singlepart, "MP_HELPER", "LONG")
arcpy.management.CalculateField(singlepart, "MP_HELPER", "!ORIG_FID!")
Minumum Bounding Geometry
Now we run the Minimum Bounding Geometry tool on our singlepart geometry.
## Step 2. get MBG envelope for all features
envelopes = arcpy.management.MinimumBoundingGeometry(singlepart, "memory\\envelopes","ENVELOPE")
## clean-up memory workspace
arcpy.management.Delete(singlepart)
Get Multipart Geometries per OID
The more difficult part now, we want to take all the singlepart envelope geometries that represented a multipart feature in our in_features and create a multipart envelope with the geometries, and then apply it to our records in the memory feature class. In the snippet below we gather the geometry information and create a dictionary where the OID from MP_HELPER is the key and the reconstructed Polygon is the value.
## get a set of unique ORIG_FIDs from the MP_HELPER field
unique_oid = set(sorted(row[0] for row in arcpy.da.SearchCursor(envelopes, "MP_HELPER")))
## for each oid
for oid in unique_oid:
## create a list to hold the arrays for the geometry
arrays = []
## iterate through each record that has the same MP_HELPER ID
with arcpy.da.SearchCursor(envelopes, ["MP_HELPER", "SHAPE@"], "MP_HELPER = {0}".format(oid)) as cursor:
for row in cursor:
## access the geometry
geometry = row[1]
## create an Array object
array = arcpy.Array()
## add all the points for each part to the array
for part in geometry:
for point in part:
array.add(point)
## add that array to our array list
arrays.append(array)
## create the dictionary entry for the oid, the value is the Polygon geometry
## that creates the multipart
poly_dict[oid] = arcpy.Polygon(arcpy.Array(arrays))
## clean-up memory workspace
arcpy.management.Delete(envelopes)
Apply Multipart Geometries to Features in the Memory Feature Class
And below is where we apply the geometry to the records in our memory workspace feature class.
## get the ORIG_OID field for the memory_fc
## this could differ from the in_features
orig_fld = [fld.name for fld in arcpy.ListFields(memory_fc) if fld.name=="ORIG_OID"][0]
## use the UpdateCursor to update the geometry for each record
with arcpy.da.UpdateCursor(memory_fc, [orig_fld, "SHAPE@"]) as cursor:
for row in cursor:
row[1] = poly_dict[row[0]]
cursor.updateRow(row)
## no need for our ORIG_OID field to remain...unless you want it to.
arcpy.management.DeleteField(memory_fc, "ORIG_OID")
Export from the Memory Workspace
Export the feature class and save to disk.
## save to disk
arcpy.CopyFeatures_management(memory_fc, out_feature_class)
Cleanup the Memory Workspace
## clean up memory workspace.
arcpy.management.Delete(memory_fc)
Important Note
The follow excerpt from the Esri documentation is important to note.
Keep in mind that parts in a multipart polygon are spatially separated. They can touch each other at vertices, but they cannot share edges or overlap. When you are sketching a multipart polygon, any parts that share an edge will be merged into a single part when you finish the sketch. In addition, any overlap among parts will be removed, leaving a hole in the polygon.
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. Right-click on your toolbox/toolset of choice and select New > Script. The New Script window will appear. In the General tab set Name to featureEnvelopeToPolygon, Label to Feature Envelope to Polygon (Basic), and the Description to Feature Envelope to Polygon with a Basic License.
In the Parameters tab set as per below. Set the Filter for the Input Features parameter to be Multipoint, Polygon, and Polyline Feature Type. Set the Filter for Output Feature Class to Polygon Feature Type, and set the Direction to Output. Set the Filter for the Create multipart features parameter to a Value List containing SINGLEPART and MULTIPART and set the Default to SINGLEPART
In the Execution tab, click the folder icon in the top-right corner and ad your saved Python script.
In the Validation tab enter the following to add an error message when SINGLEPART is chosen with a Multipoint feature class, and click OK when finished.
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 == "Multipoint" and selection == "SINGLEPART":
self.params[0].setErrorMessage("Cannot use SINGLEPART with Multipoint")
elif shape_type == "Multipoint" and selection == "MULTIPART":
self.params[0].clearMessage()
return
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/arcpy/functions/getparameterastext.htm
## https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/minimum-bounding-geometry.htm
## https://pro.arcgis.com/en/pro-app/latest/arcpy/functions/listfields.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/arcpy/functions/describe.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/multipart-to-singlepart.htm
## https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/add-field.htm
## https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/calculate-field.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/array.htm
## https://pro.arcgis.com/en/pro-app/latest/arcpy/classes/polygon.htm
## https://pro.arcgis.com/en/pro-app/latest/arcpy/data-access/updatecursor-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
##
## ArcGIS Pro Version: 3.1.0
##
################################################################################
################################################################################
## USER INPUTS / PARAMETERS
## the input features that can be multipoint, line, polygon
in_features = arcpy.GetParameterAsText(0)
## the output polygon feature class
out_feature_class = arcpy.GetParameterAsText(1)
## one envelope for entire multipart of sperate envelope for each part of the
## multipart.
single_envelope = arcpy.GetParameterAsText(2)
################################################################################
## GET THE FEATURE CLASS SHAPE TYPE
## get the shape type of the in_features feature class
shape_type = arcpy.Describe(in_features).shapeType
################################################################################
## SINGLEPART and MULTIPOINT SELECTIONS
## Validation will only allow SINGLEPART to be chosen for Multipoint
if single_envelope == "SINGLEPART" or (shape_type == "Multipoint" and single_envelope == "MULTIPART"):
## we can simply use the MBG tool available for a Basic License to achieve
## desired output
arcpy.management.MinimumBoundingGeometry(in_features, out_feature_class, "ENVELOPE")
################################################################################
## MULTIPART POLYGON\POLYLINE
## if MULTIPART was selection for a Polygon or Polyline Feature Class
elif single_envelope == "MULTIPART" and shape_type in ("Polygon", "Polyline"):
############################################################################
## REQUIRED OBJECTS
## we need the OID field to aid will matching original records with output records
oid_fld = [fld.name for fld in arcpy.ListFields(in_features) if fld.type=="OID"][0]
## get a list of fields required for the output
in_fld_names = [fld.name for fld in arcpy.ListFields(in_features) if fld.type not in ("Blob","Geometry","GlobalID","Guid","OID","Raster")]
## at the OID field as the first field name in the list
in_fld_names.insert(0, oid_fld)
## get the SRS of the in_features feature class
srs_id = arcpy.Describe(in_features).spatialReference.factoryCode
## dictionary to hold {oid:geometry} this will hold all the geometry we need to
## reapply to our memory_fc
poly_dict = {}
############################################################################
## CREATE TEMPORARY FEATURE CLASS IN MEMORY WORKSPACE
## create a Polygon feature class in the memory workspace
memory_fc = arcpy.management.CreateFeatureclass("memory", "memory_fc", "POLYGON", in_features,
"SAME_AS_TEMPLATE", "SAME_AS_TEMPLATE", srs_id)
## add the ORIG_OID field
arcpy.management.AddField(memory_fc, "ORIG_OID", "LONG")
## iterate through the in_features feature class and populate the
## attribute table for feature class in memorry
with arcpy.da.SearchCursor(in_features, in_fld_names) as cursor:
in_fld_names[0] = "ORIG_OID"
for row in cursor:
with arcpy.da.InsertCursor(memory_fc, in_fld_names) as i_cursor:
i_cursor.insertRow(row)
############################################################################
## MULTIPART TO SINGLEPART out-of-the-box BASIC TOOL
## Step 1. create singlepart from multipart
singlepart = arcpy.management.MultipartToSinglepart(in_features, "memory\\single_part")
## add in a field to maintain the ORIG_FID field info generated from the
## MultipartToSinglepart tool. The MBG tool also uses ORIG_FID and it can
## cause confusion so we will be explicit and populate out own MP_HELPER field
## with the ORIG_FID
arcpy.management.AddField(singlepart, "MP_HELPER", "LONG")
arcpy.management.CalculateField(singlepart, "MP_HELPER", "!ORIG_FID!")
############################################################################
## MINIMUM BOUNDING GEOMETRY out-of-the-box BASIC TOOL
## Step 2. get MBG envelope for all features
envelopes = arcpy.management.MinimumBoundingGeometry(singlepart, "memory\\envelopes","ENVELOPE")
############################################################################
## GET MULTIPART GEOMETRIES PER OID
## get a set of unique ORIG_FIDs from the MP_HELPER field
unique_oid = set(sorted(row[0] for row in arcpy.da.SearchCursor(envelopes, "MP_HELPER")))
## for each oid
for oid in unique_oid:
## create a list to hold the arrays for the geometry
arrays = []
## iterate through each record that has the same MP_HELPER ID
with arcpy.da.SearchCursor(envelopes, ["MP_HELPER", "SHAPE@"], "MP_HELPER = {0}".format(oid)) as cursor:
for row in cursor:
## access the geometry
geometry = row[1]
## create an Array object
array = arcpy.Array()
## add all the points for each part to the array
for part in geometry:
for point in part:
array.add(point)
## add that array to our array list
arrays.append(array)
## create the dictionary entry for the oid, the value is the Polygon geometry
## that creates the multipart
poly_dict[oid] = arcpy.Polygon(arcpy.Array(arrays))
############################################################################
## APPLY THE MULTIPART GEOMETRIES TO THE RECORDS IN THE MEMORY FEATURE CLASS
## get the ORIG_OID field for the memory_fc
## this could differ from the in_features
orig_fld = [fld.name for fld in arcpy.ListFields(memory_fc) if fld.name=="ORIG_OID"][0]
## use the UpdateCursor to update the geometry for each record
with arcpy.da.UpdateCursor(memory_fc, [orig_fld, "SHAPE@"]) as cursor:
for row in cursor:
row[1] = poly_dict[row[0]]
cursor.updateRow(row)
## no need for our ORIG_OID field to remain...unless you want it to.
arcpy.management.DeleteField(memory_fc, "ORIG_OID")
############################################################################
## EXPORT FROM MEMORY TO DISK
## save to disk
arcpy.CopyFeatures_management(memory_fc, out_feature_class)
############################################################################
## CLEAN-UP the MEMORY WORKSPACE
## clean up memory workspace.
arcpy.management.Delete(memory_fc)