As you may already know, Arcade is the official ArcGIS scripting language and it is getting more powerful with each release.
In ArcGIS Pro 2.5/Enterprise 10.8 (Arcade 1.9) you can start using the new Arcade API FeatureSetByAssociation which will allow you to work with utility network associations. In this blog we will go through how we used to work with association in Arcade, then introduce the new FeatureSetByAssociation API signature and finally we will work with the API in different context such as popup and attribute rules.
The past…
Before this API, answering simple association questions such as how many devices are contained in this substation through containment associations was challenging. We would have to first open the association table. To do that, we need the name of the association table which is tricky since it is uniquely tagged with the utility network dataset id. Then we need to craft the correct filter predicate against that table to retrieve the association rows, and this is also tricky since we need to know when to use FROMGLOBALID vs TOGLOBALID and when to filter against terminals and not. Once we figure out the where clause and execute the query, we need to identify what rows belong to which class using an internal NetworkSourceID. Here is an example of Arcade script working with association prior to 2.5.
//Before the API
var associationTable = FeatureSetByName($datastore, "unadmin.gdb.un_6_association", ["FROMGLOBALID", "TOGLOBALID"])
var globalId = $feature.globalId;
var sourceId = 7 // have to know the internal network source Id of the Device class
//need to know how to build the filter predict correctly to get the content
var devicesRows = Filter(associationTable, "FROMGLOBAL = @globalId AND TONETWORKSOURCEID = @sourceId")
var devicesCount = count(devicesRows);
return devicesCount
featureSetByAssociation to the rescue
Well we know the pain, so we sat down and worked a design for a utility network specific Arcade API and here is the signature. Don’t worry we will go through examples.
featureSetByAssociation(feature,
associationType,
terminalName? string = “” (null can be used)) -> FeatureSet
feature: The feature to perform the query against
associationType: A required parameter for association type and can the following string values.
“connected” – will return the features connected to the specified feature via a connectivity association
“container” – will return the features containing the specified feature through a containment association
“content” – will return the features contained inside the specified feature through a containment association
“structure” – will return the feature the specified feature is attached to through a structural attachment association
“attached” – will return the features attached to the specified feature through a structural attachment association
terminalName: Optional parameter applicable for connectivity associations only, passing null or means ignore the terminalName. Passing the terminalName will filter against the terminal.
Data about all the associations is returned:
`objectId:` ObjectID of row in Association Table
`globalId:` Global ID of Feature in other Table (ie TOGLOBALID or FROMGLOBALID )
`isContentVisible:` 1 or 0; IsContentVisible of row in Association Table
`className:` String based on value of (TONETWORKSOURCEID or FROMNETWORKSOURCEID )
If we want to rewrite our substation example above using the new API it will look like this. $feature is the substation. See how much more elegant this is now.
var allContent = featuresetbyAssociation ($feature, "content")
var devicesRows = Filter(allContent, "className = 'Electric Device'")
var devicesCount = count(devicesRows);
return devicesCount
FeatureSetByAssociation in Popup Profile
Using Custom Popups in ArcGIS Pro we will create a set of arcade expressions on different layers. Consider the following utility network map.
Structural Association
In the Pole layer, I have configured 4 popup expressions that uses structural association as follows:
#Attached Assets
An expression that will return all assets attached to the pole being explored.
var attachedRows = FeatureSetByAssociation($feature, "attached")
return count(attachedRows);
#Attached Devices
An expression that will return all devices attached to the pole being explored.
var attachedRows = FeatureSetByAssociation($feature, "attached")
var devices = Filter(attachedRows, "className = 'ElectricDistribution Device'");
return count(devices);
#Attached Junctions
An expression that will return all Junctions attached to the pole being explored
var attachedRows = FeatureSetByAssociation($feature, "attached")
var junctions = Filter(attachedRows, "className = 'ElectricDistribution Junction'");
return count(junctions);
#Attached Assemblies
An expression that will return all Assemblies attached to the pole being explored
var attachedRows = FeatureSetByAssociation($feature, "attached")
var junctions = Filter(attachedRows, "className = 'ElectricDistribution Assembly'");
return count(junctions);
Connectivity Association
On the transformer layer I have configured two pop up expressions to demonstrate connectivity associations as follows
var connectedRows = FeatureSetByAssociation($feature, "connected", "High");
return count(connectedRows);
#Low Side Assets
An expression that returns all assets that have connectivity associations with the low side terminal of the transformer.
var connectedRows = FeatureSetByAssociation($feature, "connected", "Low");
return count(connectedRows);
Containment Association
Finally, I have a popup expression on the transformer bank layer to sum all the kVA of the content transformers. Here is how this works.
#TotalKVA
An expression defined on the transformer bank that finds all the content transformers and sums the kva field on the transformer field and returns.
var contentRows = FeatureSetByAssociation($feature, "content")
var globalIds = [];
var i = 0;
for (var v in contentRows) {
globalIds[i++] = v.globalId
}
var deviceClass = FeatureSetByName($map, "ElectricDistribution Device", ["kva"], false)
var devicesRows = Filter(deviceClass, "globalid in @globalIds")
return sum(devicesRows, "kva")
FeatureSetByAssociation in Attribute Rules Profile
Alternatively we can use FeatureSetByAssociation in an attribute rule, here is the TotalKVA expression written for attribute rules.
#TotalKVA Attribute Rule
The calculation attribute rule SumKVA is added on the device class specifically the transformer subtype to trigger on update. When a transformer kVA field is updated*, the attribute rule will fire and find the transformer bank which is the container of edited transformer. Then it will find all the content of the transformer bank and sum all kVA minus the edited transformer. That sum is then added to the new edited kvA value. Finally we use the DML capability (new on 2.4/10.7.1) on attribute rule to write the totalKVA on the transformer bank.
* We make sure to only trigger an update if the kVA field is updated by using the new $originalFeature global in Arcade in 2.5/10.8 which will give us the state of the feature before it was edited. Without $originalFeature this attribute rule will always execute regardless of any update (moving the transformer, updating any other attribute) which can lead to unnecessary executions that essentially yield the same result.
//if the kva value hasn't been updated just exit
//$originalfeature can only be used in 2.5/10.8
if ($originalfeature.kva == $feature.kva) return $feature.kva
//$feature is the transformer, find its container (the transformer bank)
var containerRows = FeatureSetByAssociation($feature, "container")
//if no container is found exit
if (count(containerRows) == 0) return $feature.kva;
//get the globalId of the transformer
var transformerGlobalId = $feature.globalId;
//get the first container you find (usually this is just one)
var transformerBankAssociationRow = first(containerRows)
//get the transformer bank globalId
var transformerBankGlobalId = transformerBankAssociationRow.globalId;
//we just have the globalId need the full transformer bank feature
var transformerBanks = FeatureSetByName($datastore, "ElectricDistributionAssembly", ["kva"], false)
var transformerBank = First(Filter(transformerBanks, "globalid = @transformerBankGlobalId"));
//now that we have the trasnformer bank feature get all its content (transformers)
var contentRows = FeatureSetByAssociation(transformerBank, "content")
//build a list of globalids of the children transformers
var globalIds = [];
var i = 0;
for (var v in contentRows) {
globalIds[i++] = v.globalId
}
//we need to pull the kva field of those transforemrs
// we also dont' want to double count the current $feature transforemr
// so we remove it from the list
var deviceClass = FeatureSetByName($datastore, "ElectricDistributionDevice", ["kva"], false)
var devicesRows = Filter(deviceClass, "globalid <> @transformerGlobalId")
var devicesRows = Filter(devicesRows, "globalid in @globalIds")
//sum all transformers kVA
var totalKVA = 0;
for (var d in devicesRows)
totalKVA+= number(d.kva);
//add the new kva value of our transformer
totalKVA += number($feature.kva);
//return the result and persist the totalKva in the bank
return {
//return the current kva as result
"result": $feature.kva,
//edit an external feature
"edit": [
{
//in this class
"className": "ElectricDistributionAssembly",
//we want to do an update
"updates": [
{
//to the assembly of globalId transformerBankGlobalId
"globalID": transformerBankGlobalId,
// and we want to update its totalKva to this value
"attributes": {"totalKva": totalKVA }
}
]
}
]
}
Is that it?
No! We are still making more enhancement to the API to address even more complex use cases. We also want to hear from you about your use cases.
Article Discussion: