ArcGIS Pro 3.5
ArcGIS Enterprise 11.5
Branch Versioning in an Enterprise Geodatabase
We are moving from ArcMap to ArcGIS Pro and as part of the migration some attribute rules are needed to replace legacy add-ins.
Below is the attribute rule for our road centerline splitting script, there is also a merge and a few others but I haven't applied those yet since this one isn't working quite right.
The attribute rule uses a point layer (RCL_SPLIT) to split the road centerlines (RCL_<County Name>), depending on the user's group it edits a different road centerline layer. It then calculates the new address ranges and edits the geometry in the RCL layer, applying the correct address ranges to the 2 new line segments.
The result should be an empty point feature class since the script deletes the point that was added. And a split polyline with the correct address ranges.
Below is the script
// Attribute Rule for Line Splitting with Address Range Interpolation
// Arcade Version 1.29
// Trigger: On Insert of Point Feature
// Rule Version: 1.0
// 06/12/2025
// RUN PARAMETERS
// Triggers : Insert, Update
// Exclude From application evaluation: True
// Configuration - adjust these to match your environment
//var roadLayerName = "RCL_{County}"; // Road Centerline layer name STILL NEED TO CHANGE IN FEATURESETBYNAME
var splitLayerName = "RCL_SPLIT";
var fromLeftField = "FromAddr_L";
var toLeftField = "ToAddr_L";
var fromRightField = "FromAddr_R";
var toRightField = "ToAddr_R";
var cutterLength = 5; // Length of perpendicular line for cutting
var brazoriaGroupID = '<groupID>'
var chambersGroupID = '<groupID>'
var coloradoGroupID = '<groupID>'
var libertyGroupID = '<groupID>'
var matagordaGroupID = '<groupID>'
var walkerGroupID = '<groupID>'
var wallerGroupID = '<groupID>'
var whartonGroupID = '<groupID>'
var hgacGroupID = '<groupID>'
function splitLineAtPoint(lineGeom, pointGeom) {
// Find the nearest location on the line to the point
var nearestInfo = PointToCoordinate(lineGeom, pointGeom);
if (IsEmpty(nearestInfo)) {
return null;
}
var pathIndex = nearestInfo.partId;
var segmentIndex = nearestInfo.segmentId;
var nearPoint = nearestInfo.coordinate;
var paths = lineGeom.paths;
var path = paths[pathIndex];
// Check if pointGeom is exactly a vertex in the path
var isVertex = false;
var vertexIndex = -1;
for (var i = 0; i < Count(path); i++) {
if (Abs(path[i].x - pointGeom.x) < 1e-8 && Abs(path[i].y - pointGeom.y) < 1e-8) {
isVertex = true;
vertexIndex = i;
break;
}
}
Console('Is Vertex: ' + isVertex);
Console('Vertex Index: ' + vertexIndex);
var firstPath = [];
var secondPath = [];
if (isVertex) {
// Split at the vertex, do not insert the split point
for (var i = 0; i <= vertexIndex; i++) {
Push(firstPath, [path[i].x, path[i].y]);
}
for (var i = vertexIndex; i < Count(path); i++) {
Push(secondPath, [path[i].x, path[i].y]);
}
} else {
// Build the first path: from start of line to the split point
for (var i = 0; i <= segmentIndex; i++) {
Push(firstPath, [path[i].x, path[i].y]);
}
// Add the split point
Push(firstPath, [nearPoint.x, nearPoint.y]);
// Build the second path: from split point to end of line
Push(secondPath, [nearPoint.x, nearPoint.y]);
for (var i = segmentIndex + 1; i < Count(path); i++) {
Push(secondPath, [path[i].x, path[i].y]);
}
}
// Create two new Polyline geometries
var line1 = Polyline({
"paths": [firstPath],
"spatialReference": lineGeom.spatialReference
});
var line2 = Polyline({
"paths": [secondPath],
"spatialReference": lineGeom.spatialReference
});
return [line1, line2];
}
// This function calculates a new from and to address based on the percentage along the line the split occurs
function newToFrom(from, to, percent) {
if (from == null || to == null) return [null, null];
var range = Abs(to - from);
if (range < 2) return [from, to];
var val = percent * range;
var newVal = 0;
if ((Floor(val) % 2) == 0) newVal = Floor(val);
else if ((Ceil(val) % 2) == 0) newVal = Ceil(val);
else newVal = Floor(val) - 1;
if (newVal == range) newVal -= 2;
if (from > to) return [from - newVal, from - newVal - 2];
else return [from + newVal, from + newVal + 2];
}
// TODO Change code so that PointToCoordinate is only ran once and the object is sent to the Cutter function
// Main rule code starts here
var pointGeom = Geometry($feature);
// Get all road features
// ========================= CHANGE THIS TO MATCH YOUR COUNTY ===========================
// Select road layer based on user groups
var usergroups = [];
var groups = GetUser().groups;
for (var i in groups){
Push(usergroups, groups[i].id);
}
var roadLayers = {};
if (Includes(usergroups, brazoriaGroupID)) {
roadLayers['RCL_Brazoria'] = FeatureSetByName($datastore, 'RCL_Brazoria',['*'], false);
}else if (Includes(usergroups, chambersGroupID)){
roadLayers['RCL_Chambers'] = FeatureSetByName($datastore, 'RCL_Chambers',['*'], false);
}else if (Includes(usergroups, coloradoGroupID)){
roadLayers['RCL_Colorado'] = FeatureSetByName($datastore, 'RCL_Colorado',['*'], false);
}else if (Includes(usergroups, libertyGroupID)){
roadLayers['RCL_Liberty'] = FeatureSetByName($datastore, 'RCL_Liberty',['*'], false);
}else if (Includes(usergroups, matagordaGroupID)){
roadLayers['RCL_Matagorda'] = FeatureSetByName($datastore, 'RCL_Matagorda',['*'], false);
}else if (Includes(usergroups, walkerGroupID)){
roadLayers['RCL_Walker'] = FeatureSetByName($datastore, 'RCL_Walker',['*'], false);
}else if (Includes(usergroups, wallerGroupID)){
roadLayers['RCL_Waller'] = FeatureSetByName($datastore, 'RCL_Waller',['*'], false);
}else if (Includes(usergroups, whartonGroupID)){
roadLayers['RCL_Wharton'] = FeatureSetByName($datastore, 'RCL_Wharton',['*'], false);
}else if (Includes(usergroups, hgacGroupID)){
roadLayers['RCL_Brazoria'] = FeatureSetByName($datastore, 'RCL_Brazoria',['*'], false);
roadLayers['RCL_Test'] = FeatureSetByName($datastore, 'RCL_Test',['*'],false);
}else{
return {"errorMessage": "User does not belong to a recognized county group"};
}
//TODO: create HGAC North and South for only 4 groups
//interate through road layers to find roadlayer that has an intersection (only really for HGAC)
var roadLayer = ''
var roadLayerName = ''
for (var rl in roadlayers){
var intersectingRoads = Intersects(roadlayers[rl], pointGeom);
// If no roads intersect, continue to next layer
if (First(intersectingRoads) == null){
continue;
}else{
roadLayer = roadlayers[rl];
roadLayerName = rl;
break;
}
}
Console(roadLayer);
Console(roadLayerName);
// Collection to store all edits
var allAdds = [];
var allUpdates = [];
// Process each intersecting road
for (var road in intersectingRoads){
var roadGeom = Geometry(road);
// Calculate distance along line
var nearestInfo = PointToCoordinate(roadGeom, pointGeom);
if (IsEmpty(nearestInfo)) {
return {"errorMessage": "Failed to locate point on road with ID: " + road.OBJECTID};
}
var splitResult = splitLineAtPoint(roadGeom, pointGeom);
if (IsEmpty(splitResult) || Count(splitResult) != 2) {
return {"errorMessage": "Failed to split road with ID: " + road.OBJECTID};
}
// Get the original address ranges
var fromLeft = road[fromLeftField];
var toLeft = road[toLeftField];
var fromRight = road[fromRightField];
var toRight = road[toRightField];
// Calculate the new address ranges based on the intersection location along the line
var geometryPercent = Length(splitResult[0], 'feet') / (Length(splitResult[0], 'feet') + Length(splitResult[1], 'feet'));
var newToFromLeft = newToFrom(fromLeft, toLeft, geometryPercent)
var newToFromRight = newToFrom(fromRight, toRight, geometryPercent)
Console('New Address Left: ' + newToFromLeft);
Console('New Address Right: ' + newToFromRight);
Console(road.globalid)
//Create data for new line
var newLineAttributes = {};
// Copy all attributes from original line
for (var attr in road) {
if (attr != "GlobalID" && attr != "OBJECTID" && attr != "shape" && attr != 'Shape_Length' && attr != 'Shape__Length' && attr != 'Shape.STLength()' && attr != 'created_user' && attr != 'created_date' && attr != 'last_edited_user' && attr != 'last_edited_date') {
newLineAttributes[attr] = road[attr];
}
}
// Update address ranges for new segment (second segment)
newLineAttributes[fromLeftField] = newToFromLeft[1];
newLineAttributes[toLeftField] = toLeft;
newLineAttributes[fromRightField] = newToFromRight[1];
newLineAttributes[toRightField] = toRight;
var updateAttributes = {};
updateAttributes[toRightField] = newToFromRight[0];
updateAttributes[toLeftField] = newToFromLeft[0];
Push(allAdds,{
"geometry": splitResult[1],
"attributes": newLineAttributes
});
Push(allUpdates,{
"globalId": road.globalID,
"geometry": splitResult[0],
"attributes": updateAttributes
})
}
Console('Adds: ' + allAdds);
Console('Updates: ' + allUpdates);
var result = {
'edit': [{
'className': roadLayerName,
'adds': allAdds,
},
{
'className': roadLayerName,
'updates': allUpdates
},{
'className': splitLayerName,
'deletes': [{
'globalID': $feature.globalID
}]
}]
};
return result;
The rule above works however the edits that the rule makes to both the RCL_SPLIT layer and the Road Centerline layer are not reflected in the ArcGIS Pro instance that made the edit. It is difficult to get the edits to display in Pro but if you access the branch in pro on a different machine, the edits are displayed.
Both layers are branch versioned and published to portal with the version management enabled. The error occurs regardless of it it is being applied to Default or a different branch.
There was a similar post here : https://community.esri.com/t5/attribute-rules-questions/attribute-rule-evaluation-on-the-backend-doe...
However a solution didn't seem to be found. I followed @HusseinNasser2 's troubleshooting advice to use fiddler to capture the applyEdits call, the response didn't have the ServiceEdits, below is the response from the RCL_SPLIT/applyEdits call:
{
"id":17,
"editMoment":1763400633713,
"addResults":[{"objectId":2904,"globalId":"{06DB0404-A5EA-44E1-8643-9C67790D206E}",
"success":true
}
The polyline is not split and the point feature is not deleted
We tested this deployment before on 3.4/11.4 with traditional versioned data, We were able to see the changes immediately if we disabled the cache for the layers. With 3.5/11.5 however the options to disable the cache are greyed out. Ideally we would be able to cache the data and have the edits shown immediately.
Are all the layers in the same service? Are you deleting the point on the insert? If so, that could cause issues. I ran into similar things in the Abandon Features rule. Deleting $feature in an update/insert call does some bad things. I ended up using a Batch Calc Rule that would find all features marked for deleting and deleting them at a later time.