Feature Envelope To Polygon (Data Management) on a Basic License with ArcPy

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.

Interested in learning ArcPy? check out our course.

The syntax for the Feature Envelope to Polygon tool is shown below.

arcpy.management.FeatureEnvelopeToPolygon(in_features, out_feature_class, {single_envelope})

We start by importing the ArcPy module.

import arcpy

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)

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

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

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)

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.

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)

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)

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 the feature class and save to disk.

    ## save to disk
    arcpy.CopyFeatures_management(memory_fc, out_feature_class)

And clean-up our memory workspace.

    ## clean up memory workspace.
    arcpy.management.Delete(memory_fc)

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.

Esri, creating-and-editing-multipart-polygons

For now, save your script and let’s create the custom tool in ArcGIS Pro.

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

Below is the code for the tool in its entirety.

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)

Leave a Comment

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