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
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
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
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
)
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")