ArcGIS Blog

Arcade

ArcGIS Utility Network

Snapping with attribute rules

By Koya Brown

Recently, I saw this example from parcel fabric to connect lot lines and snap to control for accuracy when editing. The use case was, essentially, when drawing parcels per their metes and bounds, preserve the COGO measurements while making sure the lot lines connect to the points. More generically, the rule can be expanded to allow snapping of a line with more than two vertices or specifying a subtype.

For example, I have a device feature class that has three subtypes: switch, transformer, and fuse. And I only want the line to snap to the nearest switch. In this case, I use the attribute rule to help me do that. Or, maybe, you want to ensure a connection between a manhole and a sewer line.

I repurposed the snapping with attribute rule by expanding the use case. Now, the new rule accounts for a polyline instead of a segment. Also, I change the logic of the rule to connect to the closest vertex. Instead of the nearest first vertex. Lastly, the expanded rule will allow specifying subtypes. I explore all of the changes to the snapping rule here.

 

Snapping with attribute rules
attribute rules snapping demo

Snapping

Snapping is an aid that uses snap agents to control for accuracy while editing and it is configurable. It is often enabled, by the user, when you set up your editing workspace before you start an edit session. This is useful when you have high volume editing workflows that require connectivity between features.

For example, connecting points and lines. Hence, the workflow here uses attribute rules instead of the snap agents. Because it works everywhere and minimizes room for error.

Polyline versus segment snapping

The original design of the attribute rule was for a line with two vertices (or segment), a start and end point. However, I wanted to be able to use the rule regardless of the number of vertices. There is a condition set to evaluate the geometry, do a count, and filter all two-point lines. Additionally, the polyline JSON has the paths of the line with the starting and ending vertex.

Construct polyline

To make the rule more generalizable, these constraints would need to be removed, which means being able to accept all polyline edits from an editor. There are two considerations to change this: the construction of the polyline and the nearest vertex.

In constructing the polyline, I create a variable for the new path. Then loop through each path by its index position, to push each individual path to the new path variable. With one exception, skipping the last path. The last path will now be the updated end point. To get the end point value, I use the target vertex coordinates, which is much like the original attribute rule. This process results in the path parameter needed for the Polyline function.

First Nearest vertex

The original example a chain operation of First and Intersect returns the first feature in the FeatureSet followed by the NearestVertex function. A limitation of this is that the line will not snap to the closest vertex due to the First function used in the chain operation. Because it will snap to the nearest vertex of the first feature returned in the FeatureSet. Meaning, if I have two points within my tolerance distance, it will snap to the first feature vertex returned not the closest one.

Closest vertex

To make the rule more flexible, the chain operation does not use the First function to get the devices. Alternatively, to identify devices for evaluation, I find the minimum distance between the features. So, I return all the devices within the search distance. Next, check the distance of each device to the last vertex of the line. This allows the line to snap to the closest device.

Make the attribute rule extensible

Making the attribute rule extensible, in a sense, allows editors to modify parameters needed for evaluation and accounts for potential failures. Here, I use a table to store the parameters that users can modify to suit their editing experience. This alone does not make the rule extensible.

To make it extensible, meaning, the rule will not fail if the values in the table are null: I add a logical condition. The expression will use default values if the configuration table is empty. Therefore, editors can set their search distance, the distance units, and a subtype. This will be discussed in the next section. If these values are not set by the editor, the rule will automatically use default values.

Specify subtypes

Subtypes can be applied to a rule as part of the Attribute Rules view configuration. But I use the FilterBySubtypeCode function. And the value comes from the editors’ input into the snapping configuration table.

Let’s say, I have a rail line and will snap to the closest switch that has a subtype code ‘0’. This value will be put into the configuration table and passed as a parameter. The chain operation will get the table and filter the table by the subtype code. By adding the parameter, to the snapping configuration table, it allows the attribute rule to be both specific and flexible.

Final output

You now know how to make an attribute rule more generalizable by making a few modifications to expand the rule’s applicability. I also showed how using a configuration table helps to create a more dynamic rule. Additionally, you can enable editor flexibility by allowing input parameters like feature subtype.

For more tips, check out the Esri Community blog article on authoring attribute rules and configurations best practices.

Share this article

2 responses to “Snapping with attribute rules”

  1. Really awesome use case for geometry updates in an attribute rule! I got inspired by your script and did some minor modifications that allow for up to 10 hierarchical snapping rules so it can behave kinda like the existing Snap tool:

    // Provide a configurable Line -> Point snapping rule that reads from a configuration table
    // Table:
    //      Name: ‘SnapSettings’
    //      Fields: {
    //                  SearchDistance: float,
    //                  SearchUnits: str, LineFeatures: str [fc_name],
    //                  PointFeatures: str [fc_name],
    //                  Priority: int [0-10] (this is used to reconsile multiple snap rules)
    //              }
    //      Optional: Add domain to search units to limit options to valid unit strings
    //
    // Usage:
    //     1) Create the config table in the same database as the features used in the rule
    //     2) Fill out some base snapping options for the features (name, distance, priority, etc.)
    //     3) Add this rule to any line features that need snapping
    //     4) Start drawing and watch the snapping magic happen
    //

    // Set a mapping of valid point feature names to FeatureSets
    // Arcade requires FeatureSets to be referenced by a raw string literal so you have
    // to define which points you want to make available here:
    var validPointFeatures = {
        ‘point1’: FeatureSetbyName($datastore, ‘point1’, [‘OBJECTID’], true), // CHANGEME
        ‘point2’: FeatureSetbyName($datastore, ‘point2’, [‘OBJECTID’], true), // CHANGEME
    }

    // Grab the current featureclass name
    var featureName = ‘lines’ // CHANGEME
    var snappingConfigs = Filter(
        FeatureSetbyName(
            $datastore,’SnapSettings’,[‘LineFeatures’, ‘SearchDistance’, ‘SearchUnits’, ‘PointFeatures’, ‘Priority’], false
        ),
            ‘LineFeatures = @featureName’
        )

    // Early return if no configs exist
    if (Count(snappingConfigs) == 0) { return }

    // Define the snapping function that will be run on all supplied configs
    function snap(to, dist, units) {
        var geom = Geometry($feature)
        var lastPath = geom[‘paths’][-1]

        // Filter by subtype and Set ‘Points’ to the fully qualified table name
        var closest = []
        var points = DefaultValue(validPointFeatures, to, null)
        if (points == null) { return null }
       
        var lastClosest
        for (var i=-1; i < 1; i++) { // Iterate from -1 to 0 for start and end points
            var closestPoints = Intersects(points, Buffer(lastPath[i], dist, units))
            var minDistance = Infinity
            var closestPoint = null

            for (var pnt in closestPoints){
                if (lastClosest != null && Equals(Geometry(lastClosest), Geometry(pnt))) { continue } // Don’t use same point for both ends
                var pointDistance = Round(Distance(pnt, Point(lastPath[i]), units), 3)
                if (pointDistance < minDistance) {
                    closestPoint = Geometry(pnt)
                    minDistance = pointDistance
                    lastClosest = closestPoint
                }
            }
            Push(closest, closestPoint)
        }

        // Nothing to snap too
        if (closest[0] == null && closest[1] == null) {return null}

        geom = Dictionary(geom)
        var newPath = Array(geom[‘paths’][-1])

        // Snap last point
        if (closest[0] != null) {
            newPath[-1] = closest[0]
        }

        // Snap first point
        if (closest[1] != null) {
            newPath[0] = closest[1]
        }
        geom[‘paths’][-1] = newPath
        return Geometry(geom)
    }

    // Initialize slots for 10 hierarchical snaps (priority 0-9) only first non-null snap will be used
    var snapHierarchy = [null, null, null, null, null, null, null, null, null, null]

    var searchDistance
    var searchUnits
    var pointFeatures
    var priority
    for (var config in snappingConfigs) {
        // Don’t bother computing lower priority snaps if a higher priority one has already been found
        searchDistance = config[‘SearchDistance’]
        searchUnits = config[‘SearchUnits’]
        pointFeatures = DefaultValue(validPointFeatures, config[‘PointFeatures’], null)
        if (pointFeatures == null) { continue } // Skip invalid rows
        priority = config[‘priority’]
        // skip invalid priority slots
        if (priority == null || priority > 9) { continue }
        snapHierarchy[priority] = snap(pointFeatures, searchDistance, searchUnits)
    }

    // Iterate candidates in order
    for (var candidate in snapHierarchy) {
        // Skip non-set candidates
        if (snapHierarchy[candidate] == null) { continue }
        break
    }

    if (snapHierarchy[candidate] != null) {
        return {
            ‘result’: {
                ‘geometry’: snapHierarchy[candidate]
            }
        }
    }

    return {
        ‘result’: {
            ‘geometry’: Geometry($feature)
        }
    }

  2. Great article!
    I really appreciate how you expanded the snapping attribute rule to support full polylines, closest vertex logic, and subtype filtering. Using a configuration table for dynamic parameters is especially helpful for flexible editing workflows. This approach clearly reduces manual errors and improves accuracy during high-volume edits. Thanks for sharing!

Leave a Reply