Arcade Code Review: Set polyline M-values to cumulative length of line

2307
13
03-11-2022 10:19 AM
Bud
by
Notable Contributor


I have an Arcade calculation attribute rule that sets polyline M-values to the cumulative length of the line.

 

Bud_1-1647021579427.png

The script works as expected. But I'm still a novice, so I thought I'd ask: Can the script be improved? 


Notes about the script:

- It works for both singlepart and multipart features.
- It densifies true curves (unavoidable, since Arcade pre-densifies true curves).
- The code below is out-of-date. I've posted an updated version in a reply...further down in this post.

//var paths = [[[0, 5, null], [10, 10, null], [30, 0, null], [50, 10, null], [60, 10, null]]];  //JS

//Only do the calculation if the geometry was changed/edited.
if (!Equals(Geometry($feature), Geometry($originalfeature))) { 
    var geom = Dictionary(Text(Geometry($feature))); 
    var paths = geom['paths']; 

    //Alternatively, this could be done with the Arcade distance() function. I chose to do the math manually so that the script can be used elsewhere, such as VSCode (for debugging).
    function pythagoras(x1, y1, x2, y2) {
        return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));
    }
    for (var path_idx in paths) {
        var oldX = paths[0][0][0], oldY = paths[0][0][1], length = 0;
        for (var point_idx in paths[path_idx]) {
            var newX = paths[path_idx][point_idx][0], newY = paths[path_idx][point_idx][1];
            length += pythagoras(oldX, oldY, newX, newY);
            paths[path_idx][point_idx][2] = length;
            oldX = newX;
            oldY = newY;
        }
    }  
    console(paths);
    return { "result": { "geometry": Polyline(geom) } };

    //Output: [[[0,5,0],[10,10,11.180339887498949],[30,0,33.54101966249685],[50,10,55.90169943749475],[60,10,65.90169943749476]]]
}  

(The script was adapted from this GitHub sample: Set Ms to Index.)


Thanks!

Tags (1)
0 Kudos
13 Replies
MikeMillerGIS
Esri Frequent Contributor

Looks pretty good, minor tweaks and comments

 

//Only do the calculation if the geometry was changed/edited.
// I like to exit early when condition is not met, makes the code a little more readable
if (Equals(Geometry($feature), Geometry($originalfeature))) {
return
}
var geom = Dictionary(Text(Geometry($feature)));
var paths = geom['paths'];

//Alternatively, this could be done with the Arcade distance() function. I chose to do the math manually so that the script can be used elsewhere, such as VSCode (for debugging).
function pythagoras(x1, y1, x2, y2) {
return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));
}

// Easier to read when on multi lines and moved out of the loop, it was being reset on each part, not sure that is what you wanted
var oldX = paths[0][0][0];
var oldY = paths[0][0][1];
// dont use length, that is a reserved function name
var line_length = 0;

for (var path_idx in paths) {
for (var point_idx in paths[path_idx]) {
var newX = paths[path_idx][point_idx][0];
var newY = paths[path_idx][point_idx][1];
line_length += pythagoras(oldX, oldY, newX, newY);
// can use a negative slice, will work when there is no Z
paths[path_idx][point_idx][-1] = line_length;
oldX = newX;
oldY = newY;
}
}
console(paths);
return { "result": { "geometry": Polyline(geom) } };

//Output: [[[0,5,0],[10,10,11.180339887498949],[30,0,33.54101966249685],[50,10,55.90169943749475],[60,10,65.90169943749476]]]
}
//Generic JS version: https://jsfiddle.net/5m1c69g3/

 

Bud
by
Notable Contributor

Thanks very much Mike. That really helps.

0 Kudos
Bud
by
Notable Contributor

Hi Mike, here's my latest version, in case you're interested. I used your suggestions.

 

//Script title: Set polyline M-values to cumulative length of line

var geom = Dictionary(Text(Geometry($feature)));
var paths = geom['paths'];

//Using the Equals() function doesn't work as expected. See: Arcade: Editing M-values not treated as change to geometry (https://community.esri.com/t5/arcgis-pro-questions/arcade-editing-m-values-not-treated-as-change-to/m-p/1153917)
//if (Equals(Geometry($feature), Geometry($originalfeature)) ) { 
if (Text(Geometry($feature)) == Text(Geometry($originalfeature))) {
    console("The geometry has not changed. We don't need to redefine the M-Values.")
    return
} else {
    //Alternatively, this could be done with the Arcade distance() function. I chose to do the math manually so that the script can be used elsewhere, such as VSCode (for debugging).
    function pythagoras(x1, y1, x2, y2) {
        return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));
    }

    var oldX = paths[0][0][0];
    var oldY = paths[0][0][1];
    var line_length = 0;

    for (var path_idx in paths) {
        for (var point_idx in paths[path_idx]) {
            var newX = paths[path_idx][point_idx][0];
            var newY = paths[path_idx][point_idx][1];
            //For multi-part features, we want the M-value for additional parts to start at the last M-value from the last part. We don't want to measure the distance *between* parts.
            //So, for the first vertex in a given part, we will skip calculating the length between vertices.
            if (point_idx != 0) {
                line_length += pythagoras(oldX, oldY, newX, newY);
            }
            //We can use a negative slice: it will work when there is no Z.
            paths[path_idx][point_idx][-1] = line_length;
            oldX = newX;
            oldY = newY;
        }
    }
    console("The geometry has changed. So we *will* redefine the M-Values.")
    return { "result": { "geometry": Polyline(geom) } };
}  

 

@JohannesLindner might find this interesting.

Cheers!

0 Kudos
MikeMillerGIS
Esri Frequent Contributor

The else is redundant, easier to read without the indent

 

//Script title: Set polyline M-values to cumulative length of line

var geom = Dictionary(Text(Geometry($feature)));
var paths = geom['paths'];

//Using the Equals() function doesn't work as expected. See: Arcade: Editing M-values not treated as change to geometry (https://community.esri.com/t5/arcgis-pro-questions/arcade-editing-m-values-not-treated-as-change-to/m-p/1153917)
//if (Equals(Geometry($feature), Geometry($originalfeature)) ) { 
if (Text(Geometry($feature)) == Text(Geometry($originalfeature))) {
    console("The geometry has not changed. We don't need to redefine the M-Values.")
    return
}  
//Alternatively, this could be done with the Arcade distance() function. I chose to do the math manually so that the script can be used elsewhere, such as VSCode (for debugging).
function pythagoras(x1, y1, x2, y2) {
    return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));
}

var oldX = paths[0][0][0];
var oldY = paths[0][0][1];
var line_length = 0;

for (var path_idx in paths) {
    for (var point_idx in paths[path_idx]) {
        var newX = paths[path_idx][point_idx][0];
        var newY = paths[path_idx][point_idx][1];
        //For multi-part features, we want the M-value for additional parts to start at the last M-value from the last part. We don't want to measure the distance *between* parts.
        //So, for the first vertex in a given part, we will skip calculating the length between vertices.
        if (point_idx != 0) {
            line_length += pythagoras(oldX, oldY, newX, newY);
        }
        //We can use a negative slice: it will work when there is no Z.
        paths[path_idx][point_idx][-1] = line_length;
        oldX = newX;
        oldY = newY;
    }
}
console("The geometry has changed. So we *will* redefine the M-Values.")
return { "result": { "geometry": Polyline(geom) } };
JohannesLindner
MVP Frequent Contributor

Looks good!

As Mike said, the else is unnecessary and makes it a little harder to read. Personally, I would declare geom and paths right before oldX, so that they are closer to where you actually use them.


Have a great day!
Johannes
Bud
by
Notable Contributor

Thanks. Good suggestion about the variables. I tried to clean them up:

var geom_text = Text(Geometry($feature));

if (geom_text == Text(Geometry($originalfeature))) {
    return
}  

function pythagoras(x1, y1, x2, y2) {
    return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));
}

var geom = Dictionary(geom_text);
var paths = geom['paths'];
var oldX = paths[0][0][0];
var oldY = paths[0][0][1];
var line_length = 0;

for (var path_idx in paths) {
    for (var point_idx in paths[path_idx]) {
        var newX = paths[path_idx][point_idx][0];
        var newY = paths[path_idx][point_idx][1];
        if (point_idx != 0) {
            line_length += pythagoras(oldX, oldY, newX, newY);
        }
        paths[path_idx][point_idx][-1] = line_length;
        oldX = newX;
        oldY = newY;
    }
}
return { "result": { "geometry": Polyline(geom) } };

 

0 Kudos
MikeMillerGIS
Esri Frequent Contributor

You could use the https://developers.arcgis.com/arcade/function-reference/geometry_functions/#equals function to compare Geo instead of a text comparison.  

0 Kudos
Bud
by
Notable Contributor

Yeah, although Equals() doesn’t work the way I would have expected…it doesn’t consider changes to M-values: Arcade: Editing M-values not treated as change to geometry

So that’s why I went with the text comparison.

0 Kudos
MikeMillerGIS
Esri Frequent Contributor

Ahh good point, forgot about that.