Find Centroids (Analysis) in ArcGIS Online without Consuming Credits using the ArcGIS API for Python – Part 1

Table of Contents

Introduction

The ArcGIS API for Python is a powerful Python library that allows users to interact with and automate tasks in ArcGIS Online (or Portal). The API is excellent for programmatically creating, maintaining, and updating components of ArcGIS Online along with performing analysis tasks. In this post we will focus on the Find Centroids analysis tool and how we can use the ArcGIS API for Python to create a script to perform the same task but without consuming credits.

This is Part 1 of a two-part blog. In Part 1 we will assess the code to get the centroids and create a Feature Service that contains a Point Feature Layer for our centroid features. In Part 2 we will create a second script that allows us to truncate our Point Feature Layer and replace with updated centroids. 

Unlock the full potential of ArcGIS Online by mastering the art of efficient Content Management with the ArcGIS API for Python. In this comprehensive course, you will embark on a journey to streamline your geospatial workflows, enhance data organization, and maximize the impact of your ArcGIS Online platform.

Geospatial Professionals, GIS Analysts, Data Managers, and enthusiasts will discover the power of automation and script-based operations to efficiently manage content in ArcGIS Online. Throughout this course, you will gain practical, hands-on experience in leveraging the ArcGIS API for Python to perform a wide range of content management tasks with ease.

arcgis modules

The API provides access to your organisations ArcGIS Online via the GIS class in the gis module. This GIS class is the gateway to ArcGIS Online. We also need to import the FeatureLayer, FeatureLayerCollection, and Geometry classes. A FeatureLayerCollection object represents a Feature Service, a FeatureLayer object represents a Feature Layer within a Feature Service, and the Geometry class will provide access to our centroid functions.
				
					from arcgis.gis import GIS
from arcgis.features import FeatureLayer
from arcgis.features import FeatureLayerCollection
from arcgis.geometry import Geometry
				
			

Accessing ArcGIS Online

Our first port of call is to access your ArcGIS Online via the GIS class. There are a handful of ways to achieve access, if you are logged into your ArcGIS Online in ArcGIS Pro you can simply use "home", otherwise, another common way is to provide the ArcGIS Online URL, followed by your username and password.
				
					## Access AGOL
agol = GIS("home")
				
			
				
					## Access AGOL
agol = GIS(
    url = "https://your_organisation.maps.arcgis.com/",
    username = "Your_Username",
    password = "Your_Password"
)
				
			

Required Objects

We have two required objects, the first is a dictionary representing the Point Feature Layer we wish to create. This dictionary gets updated in the workflow. The second, is a set of field names that we do not want copied to the output.

				
					## the FeatureLayer definition to create a new Feature Layer in a Feature Service
fl_definition = {
    "type" : "Feature Layer",
    "name" : None, # this is replaced in the workflow from teh user input
    "geometryType" : "esriGeometryPoint",
    "drawingInfo": { # the symbology used in the find_centroid tool
        "renderer": {
            "type": "simple",
            "symbol": {
                "type": "esriSMS",
                "style": "esriSMSCircle",
                "color": [
                    79,
                    129,
                    189,
                    255
                ],
                "size": 10,
                "angle": 0,
                "xoffset": 0,
                "yoffset": 0,
                "outline": {
                    "color": [
                        54,
                        93,
                        141,
                        255
                    ],
                    "width": 1
                }
            }
        }
    },
    "fields" : [ # OBJECTID and ORIG_FID
        {
            "name" : "OBJECTID",
            "type" : "esriFieldTypeOID",
            "actualType" : "int",
            "alias" : "OBJECTID",
            "sqlType" : "sqlTypeInteger",
            "nullable" : False,
            "editable" : False
        },{
            "name": "ORIG_FID",
            "type": "esriFieldTypeInteger",
            "actualType": "int",
            "alias": "ORIG_FID",
            "sqlType": "sqlTypeInteger",
            "nullable": True,
            "editable": True
        }
    ],
    "indexes" : [ # required index
        {
            "name" : "PK_IDX",
            "fields" : "OBJECTID",
            "isAscending" : True,
            "isUnique" : True,
            "description" : "clustered, unique, primary key"
        }
    ],
    "objectIdField" : "OBJECTID",
    "uniqueField" : { # a unique field is required
        "name" : "OBJECTID",
        "isSystemMaintained" : True
    }
}

## system maintained fields that are not transferred to the output
## you can add more if needed. No need to add the OID field as this is
## managed in the workflow.
remove_fields = {"GlobalID", "GUID", "Shape_Length", "Shape_Area"}
				
			

The Functions

Our find_centroids_free() function has four parameters, the input_layer is a FeatureLayer object for which you want to get the centroids for, the output_name is the name of the Feature Service to create, the layer_name is the name of the Feature Layer to create within the Feature Service, and point_location determines whether the centroid must fall within the geometry of the feature or not, the default is False, which means the centroid can be outside of the feature geometry. Each line of code below is commented. The general workflow is to check to see if there is no other Feature Service with the same name, create the empty Feature Service, add a Point Feature Layer to the Feature Service with a schema defined from the input_layer, get the centroids and attribute info for each feature, add the records/features to the Point Feature Layer. Our find_centroids_free() function calls upon another function called centroid_features(), see lines 62-66, to fetch the centroid geometry and feature attributes.
				
					def find_centroids_free(input_layer, output_name, layer_name, point_location=False):
        """
        Creates a new Feature Services based on the output_name and adds a Point
        Feature Layer and populates with the Centroid information.
    
        input_layer     FeatureLayer object
        output_name     The name of the Feature Service to create
                        Or the Feature Service item to append a Feature Layer to
        layer_name      The name of the Feature Layer to create or overwrite
        point_location  True - Output points will be the nearest point to the actual centroid, but located inside or contained by the bounds of the input feature.
                        False - Output point locations will be determined by the calculated geometric center of each input feature. This is the default.
    
        Returns:
            an Item object for the Feature Service (type: Feature Layer Collection)
    
        """

        ## check if input Feature Service name already exists, if it does no we
        ## are going to create an empty service and add a point feature layer to
        ## it and add the centroid features to the feature layer
        if agol.content.is_service_name_available(
                service_name=output_name,
                service_type="featureService"
            ) == True:
    
            ## get the OID field for the input Feature Layer
            oid_field = input_layer.properties.objectIdField
    
            ## WKID of input FeatureLayer
            wkid = input_layer.properties.extent.spatialReference.latestWkid
    
            ## create and empty Feature Service
            service_item = agol.content.create_service(
                name = output_name,
                service_type = "featureService",
                wkid = wkid
            )
    
            ## update the FeatureLayer definition dictionary with the layer name
            fl_definition["name"] = layer_name
    
            ## we only want non-system-maintained fields
            fields = [dict(field) for field in input_layer.properties.fields if field["name"] not in remove_fields and field["type"] != "esriFieldTypeOID"]
    
            ## update the fields in the FeatureLayer definition dictionary.
            fl_definition["fields"] = fl_definition["fields"] + fields
    
            ## create a FeatureLayerCollection item from our empty service
            flc = FeatureLayerCollection.fromitem(
                item = service_item
            )
    
            ## add the FeatureLayer as per the fl_definition dictionary
            flc.manager.add_to_definition(
                json_dict = {"layers":[fl_definition]}
            )
    
            ## get the FeatureSet for the input FeatureLayer
            features = input_layer.query()
    
            ## convert the FeatureSet to the Centroid Features
            output_features = centroid_features(
                feature_set = features,
                oid_field = oid_field,
                point_location = point_location
            )
    
            ## re-get the newly created feature service as an Item object
            fs_item = agol.content.get(service_item.id)
    
            ## get the point feature layer as a FeatureLayer object
            fl = [fl for fl in fs_item.layers if fl.properties.name == layer_name][0]
    
            ## add the features to the Feature Layer
            fl.edit_features(
                adds = output_features
            )
    
            return agol.content.get(fs_item.id)
    
        else:
    
            return "The Feature Service name (output_name) already exists"
				
			

Our centroid_features() method, prepares the attribute information and calculates the centroid geometry returning a list of dictionaries where each dictionary defines a feature for our output feature layer.

				
					def centroid_features(feature_set, oid_field, point_location):

        ## a list to store the dictionaries representing each centroid
        output_features = []
    
        ## for each feature in the FeatureSet
        for feature in feature_set:
            ## change the OID field name to ORIG_FID
            feature.attributes["ORIG_FID"] = feature.attributes.pop(oid_field)
    
            ## remove sysntem maintained fields from the output features
            for field in remove_fields:
                feature.attributes.pop(field, None)
    
            ## select which centroid opertaion to use
            if point_location == False:
                pnt_x, pnt_y = Geometry(feature.geometry).true_centroid
            else:
                pnt_x, pnt_y = Geometry(feature.geometry).centroid
    
            ## append the updated centrod feature dictionary to the output features list
            output_features.append(
                {
                    "geometry" : {"x" : pnt_x, "y" : pnt_y},
                    "attributes" : feature.attributes
                }
            )
    
        ## return the centroid feature definitions in a list
        return output_features
				
			

Find Centroids in Action

We’re all set to test finding centroids and creating a new Feature Layer. 

				
					## Connect to ArcGIS Online
agol = GIS("home")

## The URL of the Feature Layer to Find the Centroids for
fl_url = "FL_URL"

## Create the FeatureLayer object from the URL
fl = FeatureLayer(fl_url)

find_centroids_free(
    input_layer = fl,
    output_name = "Find Centroid Feature Service",
    layer_name = "My Centroid Feature Layer",
    point_location = False
)
				
			
Find Centroids Free Analysis ArcGIS API for Python output

Go ahead and give it a go for yourself and let us know what you think in the comments.

Geospatial Professionals, GIS Analysts, and enthusiasts will discover the power of automation and script-based operations to efficiently interact and update WebMaps in ArcGIS Online. Throughout this course, you will gain practical, hands-on experience in leveraging the ArcGIS API for Python to perform a wide range of WebMap tasks with ease.

We will dissect WebMaps for all that they are and by the end of this course you will be comfortable with manipulating the JSON, which is the behind the scenes configuration of a WebMap, to produce scripts for workflows where there is currently no API Python method, such as grouping layers.

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.

				
					from arcgis.gis import GIS
from arcgis.features import FeatureLayer
from arcgis.features import FeatureLayerCollection
from arcgis.geometry import Geometry

################################################################################
## API Reference Links: (as of 2.4.0)
##  https://developers.arcgis.com/python/latest/api-reference/arcgis.gis.toc.html#gis
##  https://developers.arcgis.com/python/latest/api-reference/arcgis.gis.toc.html#arcgis.gis.ContentManager.is_service_name_available
##  https://developers.arcgis.com/python/latest/api-reference/arcgis.geometry.html#geometry
##  https://developers.arcgis.com/python/latest/api-reference/arcgis.features.toc.html#featurelayer
##  https://developers.arcgis.com/python/latest/api-reference/arcgis.features.toc.html#arcgis.features.FeatureLayer.query
##  https://developers.arcgis.com/python/latest/api-reference/arcgis.features.toc.html#arcgis.features.FeatureLayer.edit_features
##  https://developers.arcgis.com/python/latest/api-reference/arcgis.features.toc.html#featurelayercollection
##  https://developers.arcgis.com/python/latest/api-reference/arcgis.features.managers.html#arcgis.features.managers.FeatureLayerCollectionManager.add_to_definition
##  https://developers.arcgis.com/python/latest/api-reference/arcgis.features.analysis.html#find_centroids
##
##
## Syntax:
##   find_centroids(
##      input_layer, # we will use
##      point_location=False, # we will use
##      output_name=None, # we will use
##      context=None, # we will not use
##      gis=None, # we will not use, assuming logged in GIS
##      estimate=False, # we will not use as we are not consuming credits
##      future=False # we will not use
##  )
##
## Notes:
##  We will use the output_name as the Feature Service name and add a new
##  parameter called layer_name for the output Feature Layer.
##
################################################################################

## the FeatureLayer definition to create a new Feature Layer in a Feature Service
fl_definition = {
    "type" : "Feature Layer",
    "name" : None, # this is replaced in the workflow from teh user input
    "geometryType" : "esriGeometryPoint",
    "drawingInfo": { # the symbology used in the find_centroid tool
        "renderer": {
            "type": "simple",
            "symbol": {
                "type": "esriSMS",
                "style": "esriSMSCircle",
                "color": [
                    79,
                    129,
                    189,
                    255
                ],
                "size": 10,
                "angle": 0,
                "xoffset": 0,
                "yoffset": 0,
                "outline": {
                    "color": [
                        54,
                        93,
                        141,
                        255
                    ],
                    "width": 1
                }
            }
        }
    },
    "fields" : [ # OBJECTID and ORIG_FID
        {
            "name" : "OBJECTID",
            "type" : "esriFieldTypeOID",
            "actualType" : "int",
            "alias" : "OBJECTID",
            "sqlType" : "sqlTypeInteger",
            "nullable" : False,
            "editable" : False
        },{
            "name": "ORIG_FID",
            "type": "esriFieldTypeInteger",
            "actualType": "int",
            "alias": "ORIG_FID",
            "sqlType": "sqlTypeInteger",
            "nullable": True,
            "editable": True
        }
    ],
    "indexes" : [ # required index
        {
            "name" : "PK_IDX",
            "fields" : "OBJECTID",
            "isAscending" : True,
            "isUnique" : True,
            "description" : "clustered, unique, primary key"
        }
    ],
    "objectIdField" : "OBJECTID",
    "uniqueField" : { # a unique field is required
        "name" : "OBJECTID",
        "isSystemMaintained" : True
    }
}

## system maintained fields that are not transferred to the output
## you can add more if needed. No need to add the OID field as this is
## managed in the workflow.
remove_fields = {"GlobalID", "GUID", "Shape_Length", "Shape_Area"}

def centroid_features(feature_set, oid_field, point_location):

    ## a list to store the dictionaries representing each centroid
    output_features = []

    ## for each feature in the FeatureSet
    for feature in feature_set:
        ## change the OID field name to ORIG_FID
        feature.attributes["ORIG_FID"] = feature.attributes.pop(oid_field)

        ## remove sysntem maintained fields from the output features
        for field in remove_fields:
            feature.attributes.pop(field, None)

        ## select which centroid opertaion to use
        if point_location == False:
            pnt_x, pnt_y = Geometry(feature.geometry).true_centroid
        else:
            pnt_x, pnt_y = Geometry(feature.geometry).centroid

        ## append the updated centrod feature dictionary to the output features list
        output_features.append(
            {
                "geometry" : {"x" : pnt_x, "y" : pnt_y},
                "attributes" : feature.attributes
            }
        )

    ## return the centroid feature definitions in a list
    return output_features

def find_centroids_free(input_layer, output_name, layer_name, point_location=False):
    """
    Creates a new Feature Services based on the output_name and adds a Point
    Feature Layer and populates with the Centroid information.

    input_layer     FeatureLayer object
    output_name     The name of the Feature Service to create
                    Or the Feature Service item to append a Feature Layer to
    layer_name      The name of the Feature Layer to create or overwrite
    point_location  True - Output points will be the nearest point to the actual centroid, but located inside or contained by the bounds of the input feature.
                    False - Output point locations will be determined by the calculated geometric center of each input feature. This is the default.

    Returns:
        an Item object for the Feature Service (type: Feature Layer Collection)

    """

    ## check if input Feature Service name already exists, if it does no we
    ## are going to create an empty service and add a point feature layer to
    ## it and add the centroid features to the feature layer
    if agol.content.is_service_name_available(
            service_name=output_name,
            service_type="featureService"
        ) == True:

        ## get the OID field for the input Feature Layer
        oid_field = input_layer.properties.objectIdField

        ## WKID of input FeatureLayer
        wkid = input_layer.properties.extent.spatialReference.latestWkid

        ## create and empty Feature Service
        service_item = agol.content.create_service(
            name = output_name,
            service_type = "featureService",
            wkid = wkid
        )

        ## update the FeatureLayer definition dictionary with the layer name
        fl_definition["name"] = layer_name

        ## we only want non-system-maintained fields
        fields = [dict(field) for field in input_layer.properties.fields if field["name"] not in remove_fields and field["type"] != "esriFieldTypeOID"]

        ## update the fields in the FeatureLayer definition dictionary.
        fl_definition["fields"] = fl_definition["fields"] + fields

        ## create a FeatureLayerCollection item from our empty service
        flc = FeatureLayerCollection.fromitem(
            item = service_item
        )

        ## add the FeatureLayer as per the fl_definition dictionary
        flc.manager.add_to_definition(
            json_dict = {"layers":[fl_definition]}
        )

        ## get the FeatureSet for the input FeatureLayer
        features = input_layer.query()

        ## convert the FeatureSet to the Centroid Features
        output_features = centroid_features(
            feature_set = features,
            oid_field = oid_field,
            point_location = point_location
        )

        ## re-get the newly created feature service as an Item object
        fs_item = agol.content.get(service_item.id)

        ## get the point feature layer as a FeatureLayer object
        fl = [fl for fl in fs_item.layers if fl.properties.name == layer_name][0]

        ## add the features to the Feature Layer
        fl.edit_features(
            adds = output_features
        )

        return agol.content.get(fs_item.id)

    else:

        return "The Feature Service name (output_name) already exists"


################################################################################
## FIND CENTROIDS ##############################################################

## Connect to ArcGIS Online
agol = GIS("home")

## The URL of the Feature Layer to Find the Centroids for
fl_url = "FL_URL"

## Create the FeatureLayer object from the URL
fl = FeatureLayer(fl_url)

find_centroids_free(
    input_layer = fl,
    output_name = "Find Centroid Feature Service",
    layer_name = "My Centroid Feature Layer",
    point_location = False
)

################################################################################
print("SCRIPT COMPLETE")
				
			

Leave a Comment

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