I've created an attribute rule to identify two road segments that have been merged using the Merge (Editing) tool and "merge" the address ranges together. In order for it to work successfully, the user must be sure to select the FIRST segment of the two being merged in terms of line direction to preserve attributes from in the Modify Features pane (the yellow highlighted segment in the screenshot below). The attribute rule fires on Delete, since a merge should consist of a Delete and an Update (when choosing to merge into an existing feature rather than a new feature). The rule successfully identifies another feature in the roads layer that has the same street name and whose end point is equal to the start point of the deleted feature - this should be the feature that is updated during the merge. I recognize this may not work perfectly in all scenarios but for now I'm just testing if possible to get this to work at all.
Both the deleted and updated feature from the merge are correctly identified within the attribute rule, and it successfully identifies the address ranges of both and performs calculations to ensure the ranges are compatible for merging and correctly sets the final "merged" range values (keeping the from left and from right ranges from the first/updated segment, and the to left and to right ranges from the second/deleted segment).
I then try to update the range fields in the first/updated segment using the Global ID. The rule runs successfully with no errors, but despite confirming that the final calculated ranges for the merge are correct using Console messages, the updated segment has the same address ranges as it originally did after the rule fires.
I tried to set up a test rule to run on Update to see if I could determine whether the Delete or Update happens first when using the Merge editing tool but couldn't get the Console messages to print in the Diagnostic Monitor log, although that may just be an issue with the content of the Arcade expression. I'm wondering if the Delete happens first so the feature to update as part of the merge is correctly updated, and then the Update happens so it somehow reverts to the original state from the merge.
Does anyone have any deeper insight into the inner workings of the Merge editing tool or suggestions on how this could potentially work?
@HusseinNasser2 I have sat in on a number of your attribute rule presentations at the Esri Dev & Tech Summit 2024 and 2025 as well as Esri UC 2025, as well as sat down with you for a bit at Dev & Tech Summit 2024 to get help with a similar attribute rule that could identify which 2 segments were just split and recalculate the ranges. This rule is sort of the opposite of that but I quickly realized it's a bit more complicated. I'm wondering if you might have any insight into this functionality and if you think it's possible to accomplish this merge functionality? Thanks in advance for any suggestions!
Below is the final portion of the Arcade expression for reference.
// The Arcade code leading up to this correctly identifies the 2 segments
// that are part of the merge, gets their attributes, and calculates
// whether the address ranges are compatible to be merged
// Compute final union ranges (deleted is SECOND, merged is FIRST)
var final_L_from = toNum(mergedFromL);
var final_L_to = toNum(deletedToL);
var final_R_from = toNum(mergedFromR);
var final_R_to = toNum(deletedToR);
// These console messages print out the correct final ranges
Console("Final L From: " + Text(final_L_from));
Console("Final L To: " + Text(final_L_to));
Console("Final R From: " + Text(final_R_from));
Console("Final R To: " + Text(final_R_to));
// This console message prints out the correct ID to be updated
Console("Merged Road ID to Update: " + Text(mergedRoad.GlobalID));
// Apply updates to the kept (merged) feature
return {
'edit': [{
'className': 'ROADS_StatePlane',
'updates': [{
'globalID': mergedRoad.GlobalID,
'attributes': {
'LFROM_Test': final_L_from,
'LTO_Test': final_L_to,
'RFROM_Test': final_R_from,
'RTO_Test': final_R_to
}
}]
}]
};
Here is a screenshot of the Console messages:
Solved! Go to Solution.
I'm not sure if the user workflow allows it but I would suggest using the option to keep the original features and let the attribute rules do the delete instead of the merge tool. This gives you more control as it turns the operation from a delete , update. To an update only and you will write the rule to trigger on update on geometry (use the triggering fields to set the rule to trigger only on the shape change)
on the update you will have the control to query the original features (since they won't be deleted) and copy any attribute you want off of it. And then later you can insert the logic to actually delete the original feature.
My guess as to why you are losing your update is as you suspected the order of the operations causes your update to be lost/ not persisted.
I authored an example attribute rule to demonstrate this on merge. When the user selects two line features and wants to merge, the attribute rule will pick the largest line segment and copy the addresstext of that line feature to the new merged feature and then the rule will delete the old two segments.
Hope this helps
//When merging with the option "new feature" the rule will be executed on insert, if we enabled keep original features we get access to the original rows , this way we can query the merged lines, pick the largest segment , take the address from it and copy it to the new merged line. after merging we issue a delete to delete the two original segements.
var g = $feature.GlobalID
var fs = contains(filter($featureset, "globalid <> @g"), geometry($feature))
var biggerAddress = null;
var biggerLine = null;
var tobeDeleted = []
for (var g in fs)
{
push(tobeDeleted, {"globalId": g.globalid} )
if (biggerLine == null){
biggerLine = g;
biggerAddress = g.addresstext;
}
if (length(g) >= length(biggerLine)){
biggerLine = g;
biggerAddress = g.addresstext;
}
}
if (biggerLine == null) return;
return {
"result": {"attributes": {"addresstext": biggerAddress}},
"edit": [{
"className": "main.theline",
"deletes": tobeDeleted
}]
}
Hi @BriannaF,
So there are a couple of ways around it so here are some options.
var RH = $feature.R_from
var RL = $feature.R_to
var LH = $feature.L_from
var LL = $feature.L_to
// Filter featureset for matching streetnames
var ObjID = $feature.OBJECTID
var StreetName = $feature.<stname>
var FS = Filter( $featureset, '<insert stname field> = @StreetName AND OBJECTID <> @ObjID ' )
if( Count( FS ) > 0 ){
FS = Intersects( $feature, FS )
if( Includes( ['FeatureSet','Feature'], TypeOf(FS) ){
RL = Min(FS,R_to)
LL = Min(FS,L_to)
}
}
// Compute final union ranges (deleted is SECOND, merged is FIRST)
var final_L_from = toNum(mergedFromL);
var final_L_to = toNum(deletedToL);
var final_R_from = toNum(mergedFromR);
var final_R_to = toNum(deletedToR);
// These console messages print out the correct final ranges
Console("Final L From: " + Text(final_L_from));
Console("Final L To: " + Text(final_L_to));
Console("Final R From: " + Text(final_R_from));
Console("Final R To: " + Text(final_R_to));
// This console message prints out the correct ID to be updated
Console("Merged Road ID to Update: " + Text(mergedRoad.GlobalID));
// Apply updates to the kept (merged) feature
return {
'edit': [{
'className': 'ROADS_StatePlane',
'updates': [{
'OBJECTID': mergedRoad.OBJECTID,
'attributes': {
'LFROM_Test': final_L_from,
'LTO_Test': LL,
'RFROM_Test': final_R_from,
'RTO_Test': RL
}
}]
}]
};This is just a suggestion and I can give you another but without seeing the full code this should hopefully point you in the right direction.
Another would be to convert the overlapping vertices to points and finding the lowest values based on the points.
Hi, thanks for the suggestions! I didn't want to bog down the post with the full code so my bad for not making it clear about how I'm identifying the segments. I'm actually already using a filter on street name to get the second segment that's part of the merge along with the deleted segment (which would be the current feature when this attribute rule fires because it fires on Delete). Then, I check that it's the correct segment by verifying that this feature's end point is equal to the deleted feature's start point. So I'm able to successfully identify the 2 features that are part of the merge which was the hardest part to wrap my head around.
My issue now is that despite identifying the correct segments and correctly calculating what the updated address ranges should be from the 2 segments' attributes, it does not correctly update the segment when I try to do a merge. Rather, it keeps the original ranges from the merged segment. So in the screenshot example, the merged segment ends up with the original first segment's ranges of 300-306 and 301-307, instead of the "merged" ranges of 300-398 and 301-399.
I suspect it has something to do with the order of operations of Delete/Update when I do the Merge edit, along with the order of operations of what happens with the attribute rule (which fires on Delete), but I'm not sure.
I think the only way for it to work and "grab" the address ranges from the segment that gets deleted in the merge is to have the attribute rule fire on Delete instead of Update, because if it fires on the Updated feature, the attributes from the deleted feature would already be lost. I'm not sure there is a solution to this or if I can get it to work at all, but just thought I'd ask. Thanks for your suggestions!
That makes it a bit more clear now. So what you can do is set the condition to identify the original feature in conjunction with the edit context. With the $originalfeature you can identify the feature before it's deleted and $editcontext.editType to be 'DELETE' so that upon deletion it pulls the original feature and then updates the new feature with the old value
I'm not sure if the user workflow allows it but I would suggest using the option to keep the original features and let the attribute rules do the delete instead of the merge tool. This gives you more control as it turns the operation from a delete , update. To an update only and you will write the rule to trigger on update on geometry (use the triggering fields to set the rule to trigger only on the shape change)
on the update you will have the control to query the original features (since they won't be deleted) and copy any attribute you want off of it. And then later you can insert the logic to actually delete the original feature.
My guess as to why you are losing your update is as you suspected the order of the operations causes your update to be lost/ not persisted.
Thanks for the suggestion - I hadn't looked too closely at the merge options so this gives me something to play around with. It looks like it won't be as smooth of a workflow as I was hoping for and will rely on the user having to choose specific settings, but this does give me hope that it might at least be possible conceptually. Thanks!
I authored an example attribute rule to demonstrate this on merge. When the user selects two line features and wants to merge, the attribute rule will pick the largest line segment and copy the addresstext of that line feature to the new merged feature and then the rule will delete the old two segments.
Hope this helps
//When merging with the option "new feature" the rule will be executed on insert, if we enabled keep original features we get access to the original rows , this way we can query the merged lines, pick the largest segment , take the address from it and copy it to the new merged line. after merging we issue a delete to delete the two original segements.
var g = $feature.GlobalID
var fs = contains(filter($featureset, "globalid <> @g"), geometry($feature))
var biggerAddress = null;
var biggerLine = null;
var tobeDeleted = []
for (var g in fs)
{
push(tobeDeleted, {"globalId": g.globalid} )
if (biggerLine == null){
biggerLine = g;
biggerAddress = g.addresstext;
}
if (length(g) >= length(biggerLine)){
biggerLine = g;
biggerAddress = g.addresstext;
}
}
if (biggerLine == null) return;
return {
"result": {"attributes": {"addresstext": biggerAddress}},
"edit": [{
"className": "main.theline",
"deletes": tobeDeleted
}]
}
Awesome, thank you! I will try this out. I appreciate your help!
The only other thing I can think of that would merge the two would be to create rule where if the conditions were met it would do the update and delete for you.
For instance:
@HusseinNasser2 may be able to speak to this but I would like to get his take on it.