How do I join lines end-to-end

7927
22
Jump to solution
06-26-2015 10:54 AM
JohnWarden
New Contributor III

I am using lat/lon coordinates to map points, then using the "Points to Line" tool to connect the points into a line, such as a road. The lat/lon coordinates are being generated separately for each county in the U.S.. Where the resulting lines are supposed to meet at the county boundary there is a small gap. The attached map illustrates the problem.

I have the same small gap where lines are supposed to join together within a county, which is also illustrated in the attachment.

Bearing in mind I will have thousands of instances where line pairs need to be joined, is there a way to do this with a few commands, or perhaps with a custom tool built in Modelbuilder.

If it helps, the line ends that need to be joined will always be very close together to begin with, they just won't be touching, so any procedure that involves performing an action based on proximity would probably work.

0 Kudos
22 Replies
JohnWarden
New Contributor III

I'm inputting lat/lon coordinates into an excel spreadsheet, drawing them as XY events, then exporting these points to a shapefile. Because I'm limited by the source data, I can only collect lat/lon for one county at a time. So I have to create the lines for County A and County B separately. Hence the reason the lines do not connect at the county line. The fact the lines are in different counties is of no importance to me, and the fact the breaks occur at the county line is immaterial. I just need to connect the lines.

After creating the lines I am projecting them using the Project tool. At no point in this process are any files merged.

A zip file with the three lines is attached

0 Kudos
DanPatterson_Retired
MVP Emeritus

Weird...I couldn't do it with the shapefile that you sent, so I right-clicked on the file, data export data, made a new shapefile, used the extend tool with 1000 ft (way too much) and it worked.  Seph's idea requires a value table to specify the relationship, I didn't check it out.

As another suggestion, it may be quicker and easier to edit the locations within the spreadsheet or the output file to ensure that the traversing lines have a common point, but that will depend upon your data structure.  Give it a try John

0 Kudos
JohnWarden
New Contributor III

Dan, after your email I was able to replicate what you did. After seeing the result, it dawned on me that the extend line tool merely extends the last line segment to the nearest line, maintaining the angle of the extended line. It does not extend a line from the last vertex to the closest vertex on the nearby line, as I had assumed. Because of this error on my part, the distance I selected for the Extend Line tool reflected the feet from the last vertex to the closest vertex on the nearby line, which was too small, thus producing no result. This is illustrated on the accompanying image. The 1,000 feet you used worked because the maximum distance was about 900 feet.

Xander, the results produced by your code are what I'm looking for, i.e., connecting the last vertex in one line with the first vertex on an adjacent line. However, as you pointed out it doesn't solve the problem of the gap that was on the right side of the illustration I provided. I didn't jump on your code because I don't know nuttin 'bout no Python (or any other programming language for that matter), which is why I was looking for another solution.  Thanks to all for the input. I consider this issue closed unless others have commentary.

Picture1.png

DanPatterson_Retired
MVP Emeritus

Also.... give Xander's code a whirl...it does work, just a filename and such to change

specifically this code in his link

  1.    
  1. fc_in = r"D:\Xander\GeoNet\Nueva carpeta\test.gdb\lines" 
  2.     fc_out = r"D:\Xander\GeoNet\Nueva carpeta\test.gdb\lines_out" 
  3.     tolerance = 150 # max distance to snap to a node

Xander Bakker  leap in any time you are available

0 Kudos
XanderBakker
Esri Esteemed Contributor

"Leaping" right now ...

So, although the OP prefers not to use python code, I wanted to know if the code works for the shapefile attached to the thread. At the first run an error occurred stating that a NoneType has no firstPoint... mmm, looking at the data it appears that the first geometry is a NoneType. Just calculate the length and see which records show up with a length of 0.

I added two more lines of code (lines 72 and 73) to avoid the errors (see code below). Now it does create a result and we are getting somewhere, but the code only accounts for snapping start and endpoints (nodes) of lines, not an end-point to a vertex. So this is the result:

It snaps the nodes together for the two case on the left (blue circles), but since the line below does not have a node the situation indicated with the red circle is not corrected. The code should be adjusted to account for these situations too.

import math

def main():
    import arcpy
    fc_in = r"C:\Forum\SnapLines\Fulton_Dekalb_Proj_GA.shp"
    fc_out = r"C:\Forum\SnapLines\Fulton_Dekalb_Proj_GA_out_v02.shp"
    tolerance = 200 # max distance to snap to a node

    # create dicts
    dct_lines = getPolylineDict(fc_in)
    dct_from, dct_to = getFromAndToNodes(dct_lines)

    # loop through polylines
    for oid, polyline in dct_lines.items():
        # search candidates from point
        pnt_from = dct_from[oid]
        dist_min = tolerance + 0.01
        pnt_found = None
        for oid_from, pnt in dct_from.items():
            if oid_from != oid:
                dist = calcDistance(pnt_from, pnt)
                if dist < dist_min:
                    dist_min = dist
                    pnt_found = arcpy.Point(pnt.X, pnt.Y)

        for oid_to, pnt in dct_to.items():
            if oid_to != oid:
                dist = calcDistance(pnt_from, pnt)
                if dist < dist_min:
                    dist_min = dist
                    pnt_found = arcpy.Point(pnt.X, pnt.Y)

        # change line from point
        if pnt_found != None:
            polyline = insertPointAtStart(polyline, pnt_found)
            dct_lines[oid] = polyline
            dct_from, dct_to = getFromAndToNodes(dct_lines)

        # search candidates to points
        pnt_to = dct_to[oid]
        dist_min = tolerance + 0.01
        pnt_found = None
        for oid_from, pnt in dct_from.items():
            if oid_from != oid:
                dist = calcDistance(pnt_to, pnt)
                if dist < dist_min:
                    dist_min = dist
                    pnt_found = arcpy.Point(pnt.X, pnt.Y)

        for oid_to, pnt in dct_to.items():
            if oid_to != oid:
                dist = calcDistance(pnt_to, pnt)
                if dist < dist_min:
                    dist_min = dist
                    pnt_found = arcpy.Point(pnt.X, pnt.Y)

        # change line end point
        if pnt_found != None:
            polyline = addPointAtEnd(polyline, pnt_found)
            dct_lines[oid] = polyline
            dct_from, dct_to = getFromAndToNodes(dct_lines)

    # create list of polylines
    lst_polylines = dct_lines.values()
    arcpy.CopyFeatures_management(lst_polylines, fc_out)

def getPolylineDict(fc_in):
    dct = {}
    flds = ("OID@", "SHAPE@")
    with arcpy.da.SearchCursor(fc_in, flds) as curs:
        for row in curs:
            if not row[1] is None:
                if row[1].length > 0:
                    dct[row[0]] = row[1]
    del row, curs
    return dct

def getFromAndToNodes(dct):
    dct_from = {}
    dct_to = {}
    for oid, polyline in dct.items():
        dct_from[oid] = polyline.firstPoint
        dct_to[oid] = polyline.lastPoint
    return dct_from, dct_to

def insertPointAtStart(polyline, pnt_found):
    sr = polyline.spatialReference
    lst_pnts = [pnt_found]
    for part in polyline:
        for pnt in part:
            lst_pnts.append(pnt)
    return arcpy.Polyline(arcpy.Array(lst_pnts), sr)

def addPointAtEnd(polyline, pnt_found):
    sr = polyline.spatialReference
    lst_pnts = []
    for part in polyline:
        for pnt in part:
            lst_pnts.append(pnt)
    lst_pnts.append(pnt_found)
    return arcpy.Polyline(arcpy.Array(lst_pnts), sr)

def calcDistance(pnt_to, pnt):
    return math.hypot(pnt_to.X - pnt.X, pnt_to.Y - pnt.Y)

if __name__ == '__main__':
    main()
SepheFox
Frequent Contributor

Hi John, I believe the snap tool is run on the entire set of features just like extend line. Check out the link I posted.

0 Kudos
DanPatterson_Retired
MVP Emeritus

I did

0 Kudos
SepheFox
Frequent Contributor

You did what?

0 Kudos
SepheFox
Frequent Contributor

As I read it, the Snap tool doesn't densify the line, but does change the geometry.

0 Kudos
DanPatterson_Retired
MVP Emeritus

that is why you want to densify it or at a minimum add extra vertices towards the end of the line since long lines will change direction and have a signi​ficant offset otherwise.  This becomes important if there is curvature you are trying to match

One use case for this tool is to rectify the differences in shared or common boundaries between two datasets by snapping the vertices in one boundary to the vertices, edges, or end points of the other. If the input features do not have enough vertices to match the exact curvature of the other boundary, vertices can be added to the input features using the Densify tool to allow for an added level of detail.

0 Kudos