Select to view content in your preferred language

Calculation Attribute Rules that check $originalFeature geometry against newly created features do no trigger on Insert

3245
5
Jump to solution
03-01-2022 12:56 AM
Jake_S
by Esri Contributor
Esri Contributor

When creating an ArcGIS Pro Calculation Attribute Rule that is triggered by an insert or update, if that includes a check of the geometry ($originalFeature) the rule does not trigger on insert.

The code below is intended to trigger on insert and update. Applied to the $feature, it updates a field value based on an intersecting feature class. It should, in theory, populate on insert or update.

 

if (Equals(Geometry($originalFeature),Geometry($feature))) {
    return $feature.SBPI_ID_NO
}
var parcels = Filter(Intersects(FeatureSetByName($datastore, "Parcel", ["RETIREDBYRECORD", "SBPI_ID_NO"], false), Centroid($feature)),'RETIREDBYRECORD IS NULL')
if (Count(parcels) > 0) {
    var select = first(parcels)
    return select.SBPI_ID_NO;
}
return null

 

The assumption is that when you create a new feature the $originalFeature geometry there is NULL (nonexistent) and when compared to $feature, which is assumed to be the new features geometry, the rule will trigger. To clarify, if geometry null/nonexistent is not equal to the new shape geometry the rule should trigger on insert. Because of the design of feature creation in Esri geodatabase (fGDB or eGDB) attribute rules that utilize $originalFeature on insert do not see a geometry change during creation and do not populate attributes.

This may indicate that during feature creation there is a ‘temporary’ object in which the geodatabase utilizes to calculate the features geometry, then the feature is created permanently in the geodatabase. The assumption is that the rule sees that there is a geometry (temporary object) and that geometry has not changed when creating the permanent geodatabase feature which the user sees.

We had to create a separate attribute rule without $originalFeature geometry to trigger on insert only. Then utilize the attribute rule below with the $originalFeature to trigger on update if there is a change to the geometry.

 

 

 

 

var parcels = Filter(Intersects(FeatureSetByName($datastore, "Parcel", ["RETIREDBYRECORD", "SBPI_ID_NO"], false), Centroid($feature)),'RETIREDBYRECORD IS NULL')
if (Count(parcels) > 0) {
    var select = first(parcels)
    return select.SBPI_ID_NO;
}
return null

 

 

 

 

 

Is this a common issue? Are we utilizing the $originalFeature global incorrectly? Can we confirm that this is in fact how features are created in a Esri Geodatabase?

0 Kudos
1 Solution

Accepted Solutions
JohannesLindner
MVP Alum

I did a quick test, your rule works for me, both in FGDB and SDE.

Possible workarounds:

  • You can use the $editcontext.editType global
  • The includeGeometry flag of FeatureSetByName should probably be true. I'm actually surprised it works when set to false, because (if I understand it correctly) there shouldn't be any geometry for the Intersects...
  • There's nothing wrong with using separate rules for insert and update. Yes, code duplication can be a bit troublesome, but having (different) insert and update procedures in the same rule can also get confusing.
// only compare with original geometry when you update, not on insert
if ($editcontext.editType == "UPDATE" && Equals(Geometry($originalFeature),Geometry($feature))) {
    return $feature.SBPI_ID_NO
}
 // set includeGeometry to true
var parcels = Filter(Intersects(FeatureSetByName($datastore, "Parcel", ["RETIREDBYRECORD", "SBPI_ID_NO"], true), Centroid($feature)),'RETIREDBYRECORD IS NULL')
if (Count(parcels) > 0) {
    var select = first(parcels)
    return select.SBPI_ID_NO;
}
return null

 


Have a great day!
Johannes

View solution in original post

5 Replies
JohannesLindner
MVP Alum

I did a quick test, your rule works for me, both in FGDB and SDE.

Possible workarounds:

  • You can use the $editcontext.editType global
  • The includeGeometry flag of FeatureSetByName should probably be true. I'm actually surprised it works when set to false, because (if I understand it correctly) there shouldn't be any geometry for the Intersects...
  • There's nothing wrong with using separate rules for insert and update. Yes, code duplication can be a bit troublesome, but having (different) insert and update procedures in the same rule can also get confusing.
// only compare with original geometry when you update, not on insert
if ($editcontext.editType == "UPDATE" && Equals(Geometry($originalFeature),Geometry($feature))) {
    return $feature.SBPI_ID_NO
}
 // set includeGeometry to true
var parcels = Filter(Intersects(FeatureSetByName($datastore, "Parcel", ["RETIREDBYRECORD", "SBPI_ID_NO"], true), Centroid($feature)),'RETIREDBYRECORD IS NULL')
if (Count(parcels) > 0) {
    var select = first(parcels)
    return select.SBPI_ID_NO;
}
return null

 


Have a great day!
Johannes
Jake_S
by Esri Contributor
Esri Contributor

Johannes,

Thanks for taking a look. I'll explore your solution. 

To my understanding includeGeometry flag should be set 'true' when you are modifying the features geometry. This concept is outlines in this video at about the 13 min mark.

Again thanks so much!

-Jake

JohannesLindner
MVP Alum

That's a very helpful video, that I missed completely, thanks!


Have a great day!
Johannes
0 Kudos
Jake_S
by Esri Contributor
Esri Contributor

@JohannesLindner Was able to test this and works like a charm. Also tested with includeGeometry flag set to false and executed. 

0 Kudos
RobinKurosawa
New Contributor

Found this thread when debugging this kind of issue. 

Confirmed the editType == update workaround works.

 

I was curious what $originalFeature actually looked like during an insert, so I logged it like so:

return {"errorMessage": Concatenate($originalFeature)}


I then called it with a subset of fields in an applyEdits/ payload:

=== PAYLOAD ===
{
  "Field_A": foo,
  "Field_B": "bar"
// there are other fields on the feature service that I omitted }


And it turns out that $originalFeature looks like this:

[
Rule name: $Attribute immediate calc,
Triggering event: Insert,
Class name: $class,
GlobalID: {D2897C65-5D69-4AD1-9654-4B07DDA992D6},
Error number: 0,
Script error: {\"geometry\":{\"x\":...,\"y\":...,\"spatialReference\":{\"latestWkid\":2249,\"wkid\":102686}},\"attributes\":{\"Field_A\":foo,\"Field_B\":bar,\"Field_C\":null}}
]

 

Which means ESRI is doing a few unexpected things on insert:
  1. $originalFeature is not null. The name "original feature" implies the prior state before an edit. On insert, there is no prior state — the feature doesn't exist yet. A null value would clearly communicate that.
  2. $originalFeature is a copy of $feature, not the payload we sent. It includes fields we didn't send, populated with defaults and nulls. This means ESRI is constructing $originalFeature from the hydrated feature, not from the raw input.
  3. $originalFeature has the same geometry as $feature. This causes Equals(Geometry($feature), Geometry($originalFeature)) to return true on insert, which is the opposite of what a developer would expect. A common pattern for calculation rules is to skip expensive spatial queries when geometry hasn't changed — this pattern silently breaks on insert because the "unchanged geometry" check passes when there was no previous geometry to compare against.
The practical effect is that any calculation rule using the geometry-comparison guard will silently skip its calculation on insert and return whatever placeholder value the client sent, with no error or warning. The lack of ability to log things in attribute rules only exacerbates this issue requiring developers to raise errors instead of doing typical logging, but maybe that's a different thread...