Select to view content in your preferred language

Need Help with Calculating 3D Length of Line Features in ArcGIS

1151
6
Jump to solution
05-22-2023 04:08 AM
StefanAngerer
Regular Contributor

Hey there,

Once again I am asking for help! I want to automatically calculate the 3D length of line features with an attribute  whenever I place a point feature on them. But here's the catch—these point features have different heights than the line features. So in the end, the line feature - after adding the point feature- should have a "L"-Shape extending the line feature in a right angle to the point feature.

Basically, what I'm aiming for is to add a vertex to the line exactly where the point feature is located in terms of x and y-coordinates, but with the same height as the line feature. This way, the point feature should become the new end/or start vertex of the line feature while still maintaining its height.

I'm hoping someone can help me out with creating an attribute rule that tackles this challenge. In the beginning I thought this was something rather simple but after thinking about this for a couple of hours I still cant wrap my head around it. 

Cheers,

Stefan

0 Kudos
1 Solution

Accepted Solutions
JohannesLindner
MVP Alum

OK, so something like this should work (replace fc name in lines 7 & 32):

// Calculation Attribute Rule on Z-enabled Point fc
// field: empty
// triggers: insert


// get first line feature in a small radius
var lines = FeaturesetByName($datastore, "TestLinesMZ", ["GlobalID"], true)
var i_line = First(Intersects(lines, Buffer($feature, 1, "meters")))
if(i_line == null) { return }

// get distances from start and end point
var line_geo = Geometry(i_line)
var point_geo = Geometry($feature)
var d_start = Distance(point_geo, line_geo.paths[0][0])
var d_end = Distance(point_geo, line_geo.paths[-1][-1])

// get the new point coordinates: x/y from the closest line end point, z from the point
var i = IIf(d_start < d_end, 0, -1)
var v = [line_geo.paths[i][i].x, line_geo.paths[i][i].y, point_geo.z, null]

// snap the point to start or end of line
var new_point_geo = {x: v[0], y: v[1], z: v[2], m: v[3], spatialReference: point_geo.SpatialReference}

// add vertex at start or end of line
var new_line_geo = Dictionary(Text(line_geo))
var j = IIf(i == 0, 0, Count(new_line_geo.paths[0]))
Insert(new_line_geo.paths[0], j, v)

return {
    result: { geometry: Point(new_point_geo) },
    edit: [{
        className: "TestLinesMZ",
        updates: [{
            globalID: i_line.GlobalID,
            geometry: Polyline(new_line_geo)
        }]
    }]
}

 

Old line

JohannesLindner_0-1684834857020.png

 

Insert a point near the end of the line, set its Z value

JohannesLindner_1-1684834958101.pngJohannesLindner_2-1684834975553.pngJohannesLindner_3-1684835019849.png

 

 

Add another point near the start of the line

JohannesLindner_4-1684835064514.png

 

 


Have a great day!
Johannes

View solution in original post

0 Kudos
6 Replies
JohannesLindner
MVP Alum

calculate the 3D length of line features

Length3D() does that.

 

add a vertex to the line exactly where the point feature is located in terms of x and y-coordinates, but with the same height as the line feature

Not trivial, but doable. You'd have to project the point onto the line(in 2D, I have some code for that here), interpolate the height at that point, then insert a new vertex with the point feature's x/y coordinates and the calculated height.

 

I don't understand these two requests:

the line feature - after adding the point feature- should have a "L"-Shape extending the line feature in a right angle to the point feature

the point feature should become the new end/or start vertex of the line feature while still maintaining its height.

The first request sounds like you want something like this (black: original line, blue: point feature, red: edited line, 2D):

JohannesLindner_0-1684771499082.png

The second request sounds like you want to either split the line at the new vertex or to snap the point to a line end or something completely else.

 

Could you explain more clearly, maybe supply a sketch?


Have a great day!
Johannes
StefanAngerer
Regular Contributor

Hi Johannes,

thanks for the quick respond! I tried to make a small skatch that show's what I am trying to achive:

StefanAngerer_0-1684824913891.png

Basically, at the beginning we have the old line, with a start- and end point at the same height (e.g. -1m). Lets assume the distance between those two points is also 1m, so the length of the old line is 1m.

Now somebody adds a point feature to the line, but this point feature is not at the same height as the line (e.g. 0m). So what I want the rule to do is -> add this new point as a vertex of the line, and connect it vertically with the old start- or endpoint in a right angle. This was the new line would have a total length of 2m as it has 1m in horizontal length and 1m in vertical length.

I hope this makes sense now. From reading your message I have still some struggels to get an idea how this could work but I will have a deeper look into it today.

Thanks!

0 Kudos
JohannesLindner
MVP Alum

OK, so something like this should work (replace fc name in lines 7 & 32):

// Calculation Attribute Rule on Z-enabled Point fc
// field: empty
// triggers: insert


// get first line feature in a small radius
var lines = FeaturesetByName($datastore, "TestLinesMZ", ["GlobalID"], true)
var i_line = First(Intersects(lines, Buffer($feature, 1, "meters")))
if(i_line == null) { return }

// get distances from start and end point
var line_geo = Geometry(i_line)
var point_geo = Geometry($feature)
var d_start = Distance(point_geo, line_geo.paths[0][0])
var d_end = Distance(point_geo, line_geo.paths[-1][-1])

// get the new point coordinates: x/y from the closest line end point, z from the point
var i = IIf(d_start < d_end, 0, -1)
var v = [line_geo.paths[i][i].x, line_geo.paths[i][i].y, point_geo.z, null]

// snap the point to start or end of line
var new_point_geo = {x: v[0], y: v[1], z: v[2], m: v[3], spatialReference: point_geo.SpatialReference}

// add vertex at start or end of line
var new_line_geo = Dictionary(Text(line_geo))
var j = IIf(i == 0, 0, Count(new_line_geo.paths[0]))
Insert(new_line_geo.paths[0], j, v)

return {
    result: { geometry: Point(new_point_geo) },
    edit: [{
        className: "TestLinesMZ",
        updates: [{
            globalID: i_line.GlobalID,
            geometry: Polyline(new_line_geo)
        }]
    }]
}

 

Old line

JohannesLindner_0-1684834857020.png

 

Insert a point near the end of the line, set its Z value

JohannesLindner_1-1684834958101.pngJohannesLindner_2-1684834975553.pngJohannesLindner_3-1684835019849.png

 

 

Add another point near the start of the line

JohannesLindner_4-1684835064514.png

 

 


Have a great day!
Johannes
0 Kudos
StefanAngerer
Regular Contributor

Thanks so much Johannes,

helped my a lot here! However now, there are 2 cases left that I would like to tackle as well:

1. What happens when somebody snaps a line to an already existing point with different height. (Guess this is a complete new rule on the line class)

2. Getting the same behaviour for point features that are somewhere in between start- or endpoint.

I will try to find a solution for those requirments but obviously I am happy for any help you could provide me there. 🙂

Again: Thanks for the help!

Greets,

Stefan 🙂

0 Kudos
JohannesLindner
MVP Alum

Getting the same behaviour for point features that are somewhere in between start- or endpoint

For that, you replace the rule on the point fc with this:

function split_polyline_into_segments(polyline_geometry) {
    // takes the polyline_geometry and splits it into segments (Polylines that have only 2 vertices)
    // returns an array of those segments
    var sr = polyline_geometry.spatialReference
    var vertices = polyline_geometry.paths[0]
    var segments = []
    for(var s=0; s<Count(vertices)-1; s++) {
        var p0 = vertices[s]
        var p1 = vertices[s+1]
        Push(segments, Polyline({"paths": [[ [p0.x, p0.y, p0.z, p0.m], [p1.x, p1.y, p1.z, p1.m] ]], "spatialReference": sr}))
    }
    return segments
}

function project_orthogonally(point_geometry, line_geometry) {
    // returns a projection of the point_geometry onto the line_geometry
    // only start and end point of the line_geometry are considered!
    // the projection is done in 2D, the returned point_geometry has the same Z and M values as the input.
    // https://de.wikipedia.org/wiki/Orthogonalprojektion
    // https://en.wikibooks.org/wiki/Linear_Algebra/Orthogonal_Projection_Onto_a_Line
    if(line_geometry == null) { return point_geometry }
    var p = point_geometry
    var r0 = line_geometry.paths[0][0]
    var r1 = line_geometry.paths[0][-1]
    var ux = r1.x - r0.x
    var uy = r1.y - r0.y
    var lambda = ((p.x-r0.x)*ux + (p.y-r0.y)*uy) / (ux*ux + uy*uy)
    var new_p = Point({x: r0.x + lambda * ux, y: r0.y + lambda * uy, spatialReference: p.spatialReference})
    // if new_p is on the line defined by r0 and r1 but not on the actual line_geometry, snap it to the closest end point
    if(Disjoint(new_p, line_geometry)) {
        new_p = IIF(Distance(r0, p) < Distance(r1, p), r0, r1)
    }
    // add z and m value
    new_p = Point({x: new_p.x, y: new_p.y, z: p.z, m: p.m, spatialReference: p.spatialReference})
    return new_p
}



// get first line feature in a small radius
var lines = FeaturesetByName($datastore, "TestLinesMZ", ["GlobalID"], true)
var i_line = First(Intersects(lines, Buffer($feature, 1, "meters")))
if(i_line == null) { return }

// split i_line into line segments (only 2 vertices)
var segments = split_polyline_into_segments(Geometry(i_line))

// get the line segment closest to $feature
function sort_by_distance_to_feature(s1, s2) {
    return Distance(s1, $feature) > Distance(s2, $feature)
}
var closest_segment = Sort(segments, sort_by_distance_to_feature)[0]

// project point onto that segment
var sp = project_orthogonally(Geometry($feature), closest_segment)

// add vertex to the segment
// the segment has 2 vertices
// if point is on an end point, add vertext at end of line segment, else in the middle
var new_path = [closest_segment.paths[0][0], closest_segment.paths[0][-1]]
var i = When(
    Intersects(sp, new_path[0]), 0,
    Intersects(sp, new_path[1]), 2,
    1)
    Insert(new_path, i, [sp.x, sp.y, sp.z, sp.m])
var new_segment = Polyline({paths: [new_path], spatialReference: closest_segment.spatialReference})

// replace the old line segment
var i = IndexOf(segments, closest_segment)
segments[i] = new_segment

// create the new Polyline
var new_line = Union(segments)

return {
    result: {geometry: sp},
    edit: [{
        className: "TestLinesMZ",
        updates: [{
            globalID: i_line.GlobalID,
            geometry: new_line
        }]
    }]
}

 

Old line

JohannesLindner_0-1684845857110.png

 

 

Adding a point somewhere in the middle

JohannesLindner_1-1684845916861.png

 

Adding a point before the start point

JohannesLindner_2-1684846005944.png

 


Have a great day!
Johannes
0 Kudos
StefanAngerer
Regular Contributor

Hi @JohannesLindner

I have implemented the rule and for all inserting operations it works just fine. I updated the rule a bit so that you can also snap a point feature to multiple line features and all of them get updates. Also I added a part that, if the rule detects a vertex thats already at the elevated position, no additional vertex is added. The only Problem I have is when updating the point feature. For example, somebody wants to move an already existing, elevated, point feature on a line and the connection ((new vertex) should be added automatically, it just wont work. Do you have an Idea why this could be the case. It should be noted that befor this rule is triggered, there is another rule that lifts the point feature to a certain height, however no matter what I do, the point feature gets always snapped to the height of the line.

This is the code I am using right now:

// get intersecting line features in a small radius
var lines = FeaturesetByName($datastore, "PipelineLine", ["GlobalID"], true);
var i_lines = Intersects(lines, $feature);
if (i_lines == null) {
    return;
}

// create update variable
var updates = []; 

// get distances from start and end point
for (var line in i_lines) {
    var line_geo = Geometry(line);
    var point_geo = Geometry($feature);
    var d_start = Distance(point_geo, line_geo.paths[0][0]);
    var d_end = Distance(point_geo, line_geo.paths[-1][-1]);

    // get the new point coordinates: x/y from the closest line end point, z from the point
    var i = IIf(d_start < d_end, 0, -1);
    var v = [line_geo.paths[i][i].x, line_geo.paths[i][i].y, point_geo.z, null];

    for (var k = 0; k < Count(line_geo.paths[i]); k++) {
        if (line_geo.paths[i][k].x == v[0] && line_geo.paths[i][k].y == v[1] && line_geo.paths[i][k].z == v[2]) {
            return;
        }
    }

    // add vertex at start or end of line
    var new_line_geo = Dictionary(Text(line_geo));
    var j = IIf(i == 0, 0, Count(new_line_geo.paths[0]));
    Insert(new_line_geo.paths[0], j, v);
    
    var update = {
        "globalID": line.GlobalID,
        "geometry": Polyline(new_line_geo)
    };
    
    Push(updates, update);
}

return {
    edit: [{
        "className": "PipelineLine",
        "updates": updates
    }]
};
0 Kudos