Sort (Data Management) on a Basic License with ArcPy

The Sort (Data Management) tool has limited functionality on a Basic and Standard licence, you can only sort by one field and cannot use a spatial sort. We saw in this blog post how we can use the Export Features (Conversion) tool to sort a feature class or table by multiple fields, unfortunately that Export tool does not sort spatially, and that’s where Final Draft Mapping comes in to help. In this post we are going to re-create the Sort tool for use with a Basic license. You can check out the Esri documentation for the Sort tool here.

Interested in learning ArcPy, check out our course page.

Here’s the syntax for the Sort (Data Management) tool.

arcpy.management.Sort(in_dataset, out_dataset, sort_field, {spatial_sort_method})

We only need to import the ArcPy module for this one.

import arcpy

As per the Sort tool syntax we require four possible user inputs; the in_dataset is the feature class you wish to perform the sort on, we won’t bother with tables as you can use the Export Table (Conversion) tool to sort those bad boys; out_dataset will be our sorted output dataset; and sort_field is for the field or fields you wish to sort by; spatial_sort_method, if the Shape field has been supplied (we will supply as default) you need to supply a Spatial Sort Method, and if it hasn’t been used then we’re pretty mush just using the Export Features tool!

Just to note, we will only be covering four out of the five spatial sorts available; Upper Right (default), Lower Right, Upper Left, and Lower Left. The Peano Curve algorithm will be omitted, purely because I need to provide more brain power to it at another stage.

## the features to sort
in_dataset = arcpy.GetParameterAsText(0)
## the output feature class or table to create
out_dataset = arcpy.GetParameterAsText(1)
## field whose values will be used to reorder the input records
sort_field = arcpy.GetParameterAsText(2)
## spatial sort method: UR, LR, UL, LL
spatial_sort_method = arcpy.GetParameterAsText(3)

We have a few required objects to help us through the workflow as detailed in the code below.

## srs id of the in_dataset so we can assign the output the same
srs_id = arcpy.Describe(in_dataset).spatialReference.factoryCode
## all field names from the input dataset
all_fields = [fld.name for fld in arcpy.ListFields(in_dataset) if fld.type not in ("GlobalID", "Guid", "OID", "Geometry") and not fld.name.startswith("Shape_")]
## we will need to the geometry to add into the output.
all_fields.append("SHAPE@")
## add oid field to beginning of the all_fields list
oid_fld = [fld.name for fld in arcpy.ListFields(in_dataset) if fld.type == "OID"][0]
all_fields.insert(0, oid_fld)
## this list will hold the names of the fields to sort by and order by
sort_fields =[entry.split(" ") for entry in sort_field.replace("'", "").split(";")]
## required to create correct output geometry type
shape_type = arcpy.Describe(in_dataset).shapeType

Ok, let’s get into it! We need all the records from the in_dataset as a list (of attributes/geometry) in a list.

## this will be a list of list.
all_records = [list(row) for row in arcpy.da.SearchCursor(in_dataset, all_fields)]

Next, we handle if a spatial sort method has been supplied. Based on the method desired by the user, we get the bounding box coordinates for that selection at each record. For example, if Lower Left (LL) is selected, then we get the lower-left (Xmin, YMin) coordinates of the bounding extent. The bounding extent for a point will always be the centroid. For each possible spatial sort we either reverse or do not reverse the values when using the Python sorted() method. Once we have applied our sorts we remove the X and Y coordinates from our records.

## if a spatial sort method is being applied
if spatial_sort_method != "None":
    ## and the sort method us Upper Right
    if spatial_sort_method == "UR":
        ## get the Upper Right of the bounding box for all features
        ## and add as X and Y separately to the each record
        all_records = [record+[record[-1].extent.XMax, record[-1].extent.YMax] for record in all_records]
        x_reverse = True
        y_reverse = True
    ## if the sort method is Lower Right
    elif spatial_sort_method == "LR":
        ## get the Lower Right of the bounding box for all features
        ## and add as X and Y separately to the each record
        all_records = [record+[record[-1].extent.XMax, record[-1].extent.YMin] for record in all_records]
        x_reverse = True
        y_reverse = False
    ## if the sort method is Upper Left
    elif spatial_sort_method == "UL":
        ## get the Upper Left of the bounding box for all features
        ## and add as X and Y separately to the each record
        all_records = [record+[record[-1].extent.XMin, record[-1].extent.YMax] for record in all_records]
        x_reverse = False
        y_reverse = True
    ## if the sort method is Lower Left
    elif spatial_sort_method == "LL":
        ## get the Lower Left of the bounding box for all features
        ## and add as X and Y separately to the each record
        all_records = [record+[record[-1].extent.XMin, record[-1].extent.YMin] for record in all_records]
        x_reverse = False
        y_reverse = False
    ## sort by x ascending
    all_records = sorted(all_records, key=lambda row:row[-2], reverse=x_reverse)
    ## sort by y descending
    all_records = sorted(all_records, key=lambda row:row[-1], reverse=x_reverse)
    ## we're finished with the X and Y so remove from our records
    all_records = [record[0:-2] for record in all_records]

We make use of the memory workspace and create a feature class based on the shape type of the in_dataset. The in_dataset is used as a template. The ORIG_FID field is added as this will form part or the output.

## temporary feature class in memory workspace using in_dataseta as a template
temp_fc = arcpy.CreateFeatureclass_management("in_memory", "temp_fc", shape_type,
    in_dataset, "SAME_AS_TEMPLATE", "SAME_AS_TEMPLATE", srs_id)
## add ORIG_FID field
arcpy.management.AddField(temp_fc, "ORIG_FID", "LONG")

Now we insert our spatially sorted records into our memory feature class. If the Shape field was not used and there was no spatial_sort method set, we are really just inserting all our records back in as is. There might be an alternative way to achieve this, but this method allows for the ORIG_FID to be added to the output.

## remove OID field name and replace with ORIG_FID
all_fields[0] = "ORIG_FID"
## use an insert cursor on the temp_fc
with arcpy.da.InsertCursor(temp_fc, all_fields) as i_cursor:
    ## for each record in all_records
    for record in all_records:
        i_cursor.insertRow(record)

We can now write our temporary feature class from the memory workspace to disk.

## remove Shape entry from the sort_fields if in there
sort_field = [x for x in sort_fields if "shape" not in str(x).lower()]
arcpy.conversion.ExportFeatures(temp_fc, out_dataset, sort_field=sort_field)

And as always, we clean-up our memory workspace.

## clean-up memeory workspace
arcpy.management.Delete(temp_fc)

Save your script and open 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 SortLabel to Sort (Basic), and the Description to Sort with a Basic License.

In the Parameters tab set as per below. Set the Output Dataset parameter Direction to Output. Set the Dependency for the Sort Field(s) to in_dataset and a Default of SHAPE ASCENDING. Set the Default for Spatial Sort Method to UR.

The Value Table for the Sort Field(s) parameter.

And the Filter for the Sort Field(s)

And the Value List Filter for Spatial Sort Method.

In the Execution tab, click the folder icon in the top-right corner and add your saved Python script.

Click into the Validation tab and update the updateParameters and updateMessages as per below.

    def updateParameters(self):
        # Modify parameter values and properties.
        # This gets called each time a parameter is modified, before 
        # standard validation.
        field_list = [entry.split(" ")[0].lower() for entry in self.params[2].valueAsText.replace("'", "").split(";")]
        if "shape" not in field_list:
            self.params[3].value = "None"          
        return
    def updateMessages(self):
        # Customize messages for the parameters.
        # This gets called after standard validation.
        field_list = [entry.split(" ")[0].lower() for entry in self.params[2].valueAsText.replace("'", "").split(";")]
        if "shape" in field_list and self.params[3].value == "None":
            self.params[3].setErrorMessage("If Shape field selected, Spatial Sort Method cannot be None")
        else:
            self.params[3].clearMessage()             
        return

Click OK and go test out the tool.

You can download the tool and other Advanced tools with a Basic license over on this page.

Here is the code in it’s entirety

import arcpy
################################################################################
## Esri Documentation
##  https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/sort.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/latest/arcpy/data-access/searchcursor-class.htm
##  https://pro.arcgis.com/en/pro-app/latest/arcpy/data-access/insertcursor-class.htm
##  https://pro.arcgis.com/en/pro-app/latest/arcpy/classes/extent.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/data-management/add-field.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
##
## Notes:
##  Will only work if sorting spatially is the first or last entry in the
##  sort_field parameter, and will perform the spatial sort first and then the
##  attribute sorts.
##
## ArcGIS Pro Version 3.1.0
##
################################################################################
################################################################################
## USER INPUTS
## the features to sort
in_dataset = arcpy.GetParameterAsText(0)
## the output feature class or table to create
out_dataset = arcpy.GetParameterAsText(1)
## field whose values will be used to reorder the input records
sort_field = arcpy.GetParameterAsText(2)
## spatial sort method: UR, LR, UL, LL
spatial_sort_method = arcpy.GetParameterAsText(3)
################################################################################
## REQUIRED OBJECTS
## srs id of the in_dataset so we can assign the output the same
srs_id = arcpy.Describe(in_dataset).spatialReference.factoryCode
## all field names from the input dataset
all_fields = [fld.name for fld in arcpy.ListFields(in_dataset) if fld.type not in ("GlobalID", "Guid", "OID", "Geometry") and not fld.name.startswith("Shape_")]
## we will need to the geometry to add into the output.
all_fields.append("SHAPE@")
## add oid field to beginning of the all_fields list
oid_fld = [fld.name for fld in arcpy.ListFields(in_dataset) if fld.type == "OID"][0]
all_fields.insert(0, oid_fld)
## this list will hold the names of the fields to sort by and order by
sort_fields =[entry.split(" ") for entry in sort_field.replace("'", "").split(";")]
## required to create correct output geometry type
shape_type = arcpy.Describe(in_dataset).shapeType
################################################################################
## GET ALL RECORDS
## this will be a list of list.
all_records = [list(row) for row in arcpy.da.SearchCursor(in_dataset, all_fields)]
################################################################################
## SORT SPATIALLY IF REQUIRED
## if a spatial sort method is being applied
if spatial_sort_method != "None":
    ## and the sort method us Upper Right
    if spatial_sort_method == "UR":
        ## get the Upper Right of the bounding box for all features
        ## and add as X and Y separately to the each record
        all_records = [record+[record[-1].extent.XMax, record[-1].extent.YMax] for record in all_records]
        x_reverse = True
        y_reverse = True
    ## if the sort method is Lower Right
    elif spatial_sort_method == "LR":
        ## get the Lower Right of the bounding box for all features
        ## and add as X and Y separately to the each record
        all_records = [record+[record[-1].extent.XMax, record[-1].extent.YMin] for record in all_records]
        x_reverse = True
        y_reverse = False
    ## if the sort method is Upper Left
    elif spatial_sort_method == "UL":
        ## get the Upper Left of the bounding box for all features
        ## and add as X and Y separately to the each record
        all_records = [record+[record[-1].extent.XMin, record[-1].extent.YMax] for record in all_records]
        x_reverse = False
        y_reverse = True
    ## if the sort method is Lower Left
    elif spatial_sort_method == "LL":
        ## get the Lower Left of the bounding box for all features
        ## and add as X and Y separately to the each record
        all_records = [record+[record[-1].extent.XMin, record[-1].extent.YMin] for record in all_records]
        x_reverse = False
        y_reverse = False
    ## sort by x ascending
    all_records = sorted(all_records, key=lambda row:row[-2], reverse=x_reverse)
    ## sort by y descending
    all_records = sorted(all_records, key=lambda row:row[-1], reverse=y_reverse)
    ## we're finished with the X and Y so remove from our records
    all_records = [record[0:-2] for record in all_records]
################################################################################
## CREATE OUPUT FEATURE CLASS IN MEMORY ########################################
## temporary feature class in memory workspace using in_dataseta as a template
temp_fc = arcpy.management.CreateFeatureclass("in_memory", "temp_fc", shape_type,
    in_dataset, "SAME_AS_TEMPLATE", "SAME_AS_TEMPLATE", srs_id)
## add ORIG_FID field
arcpy.management.AddField(temp_fc, "ORIG_FID", "LONG")
################################################################################
## INSERT RECORDS ##############################################################
## remove OID field name and replace with ORIG_FID
all_fields[0] = "ORIG_FID"
## use an insert cursor on the temp_fc
with arcpy.da.InsertCursor(temp_fc, all_fields) as i_cursor:
    ## for each record in all_records
    for record in all_records:
        i_cursor.insertRow(record)
################################################################################
## WRITE TO DISK ###############################################################
## remove Shape entry from the sort_fields if in there
sort_field = [x for x in sort_fields if "shape" not in str(x).lower()]
arcpy.conversion.ExportFeatures(temp_fc, out_dataset, sort_field=sort_field)
################################################################################
## CLEAN-UP MEMORY WORKSPACE ###################################################
## clean-up memeory workspace
arcpy.management.Delete(temp_fc)
################################################################################

Leave a Comment

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