Unsplit/Dissolve multiple touching lines in Stream Network using ArcGIS Desktop?

I've devised a way of doing it with little bits from all of the other answers.

The scripts that @GeoJohn and @FelixIP have created may well work (if you have access to ArcGIS 10.1 and the database access functions), so if someone else had this problem please check those options out as well.

I did the following in the end:

  1. Created a very small buffer polygon around the lines (0.1 metres) to make sure there was crossover between the lines that were connected.

  2. Use the dissolve tool with the Unsplit option checked. This created individual polygons for each of the connected line groups.

  3. Create a unique ID for each polygon.

  4. Use the spatial join tool to add the unique polygon ids to each line that intersects or is within the polygons.

  5. Use the dissolve tool again to dissolve the lines based on the unique ID.

That has worked for me but like I said the scripting methods may be preferable for other people.

enter image description here


I think the best way to do this is in arcpy. The following is a complete script that will do what you need. It works by putting a small buffer around all of the stream features and indexing them(adds a field that is used later in the script to dissolve). The buffer allows for every part of a network, whether it is connected or not, to be considered part of it by using the buffer distance as a "tolerance". So... if the networks are really close together you can set the buffer distance lower.

import arcpy, os
from arcpy import env

arcpy.env.overwriteOutput = True

inFC = arcpy.GetParameterAsText(0)
outLoc = arcpy.GetParameterAsText(1)
outName = arcpy.GetParameterAsText(2)

newFC = os.path.join(outLoc, outName)

def buildWhereClause(table, field, value):
    fieldDelimited = arcpy.AddFieldDelimiters(table, field)
    fieldType = arcpy.ListFields(table, field)[0].type
    if str(fieldType) == 'String':
        value = "'%s'" % value
    whereClause = "%s = %s" % (fieldDelimited, value)
    return whereClause

bufferClass = os.path.join(outLoc, outName + "_Buffer")
singleClass = os.path.join(outLoc, outName + "_singlePart")

arcpy.Buffer_analysis(inFC, bufferClass, 500, "", "", "ALL")
arcpy.MultipartToSinglepart_management(bufferClass, singleClass)

arcpy.AddField_management(singleClass, "INDEX_ID", "LONG")

fields = arcpy.ListFields(singleClass)
fieldList = []

for nm in fields:
    fieldList.append(nm.name)

for field in fieldList:
    if field == "INDEX_ID":
        fieldCounter = 1
        with arcpy.da.UpdateCursor(singleClass, field) as cursor:
            for row in cursor:
                row[0] = fieldCounter
                cursor.updateRow(row)
                fieldCounter += 1
        del cursor
        del row

arcpy.CreateFeatureclass_management(outLoc, outName + "_Final", "POLYLINE")
finalFC = os.path.join(outLoc, outName + "_Final")

try:
    arcpy.AddField_management(finalFC, "ID_Match", "LONG")
except:
    pass

tempFC = os.path.join(outLoc, "tempFC_SE")
tempFC_2 = os.path.join(outLoc, "tempFC_SE_2")

arcpy.AddField_management(inFC, "ID_Match", "LONG")

fieldMatch = ["INDEX_ID", "OBJECTID"]
inFCMatch = ["ID_Match"]

arcpy.MakeFeatureLayer_management(singleClass, "singleClass_lyr")
arcpy.MakeFeatureLayer_management(inFC, "inFC_lyr")

with arcpy.da.SearchCursor("singleClass_lyr", fieldMatch) as cursor:
    for row in cursor:
        arcpy.SelectLayerByAttribute_management("singleClass_lyr", "CLEAR_SELECTION")
        arcpy.SelectLayerByAttribute_management("inFC_lyr", "CLEAR_SELECTION")
        where = buildWhereClause(singleClass, "OBJECTID", str(row[1]))
        arcpy.SelectLayerByAttribute_management("singleClass_lyr", "NEW_SELECTION", where)
        arcpy.CopyFeatures_management("singleClass_lyr", tempFC_2)
        arcpy.SelectLayerByLocation_management("inFC_lyr", "INTERSECT", tempFC_2)
        arcpy.CopyFeatures_management("inFC_lyr", tempFC)
        with arcpy.da.UpdateCursor(tempFC, inFCMatch) as cursor2:
            for row2 in cursor2:
                row2[0] = row[0]
                cursor2.updateRow(row2)
        del cursor2
        del row2
        arcpy.Append_management(tempFC, finalFC, "NO_TEST")
        arcpy.Delete_management(tempFC)
        arcpy.Delete_management(tempFC_2)
del cursor
del row

arcpy.Dissolve_management(finalFC, newFC, "ID_Match")

arcpy.Delete_management(finalFC)
arcpy.Delete_management(bufferClass)
arcpy.Delete_management(singleClass)

The results will not preserve fields from the original FC but with a little tweaking that is also possible.


If I understand question correctly and this is what you'd like to see enter image description here the script below will do

# Import arcpy module
import arcpy, traceback, os, sys
from arcpy import env
infc = r'D:\Scratch\river_cl.shp'
joined = r'D:\Scratch\s_joined.shp'
groupField="GROUP"
arcpy.overwriteoutput=1
try:
    def showPyMessage():
        arcpy.AddMessage(str(time.ctime()) + " - " + message)
    arcpy.SpatialJoin_analysis(infc, infc, joined, "JOIN_ONE_TO_MANY","KEEP_ALL", "", "BOUNDARY_TOUCHES")
    vFT=list(arcpy.da.TableToNumPyArray(joined,("TARGET_FID","JOIN_FID")))
    bigList=[]
    while (True):
        if len(vFT)==0:
            break
        first=list(vFT.pop(0))
        while (True):
            if len(vFT)==0:
                break
            if -1 not in first:
                m=0;toRemove=[]
                for ent in vFT:
                    f,t = ent
                    if f in first or t in first:
                        small = filter(lambda x: x not in first, ent)
                        first+=small
                        toRemove.append(m)
                    m+=1
                if len(toRemove)==0:
                    break
                toRemove.reverse()
                for m in toRemove:
                    vFT.pop(m)
            else:
                break
        bigList.append(first)

    result=arcpy.GetCount_management(infc)
    nStreams=int(result.getOutput(0))
    groups=[-1]*nStreams
    m=0
    for first in bigList:
        small = tuple(filter(lambda x: x not in [-1], first))
        arcpy.AddMessage('Group %i Contains FID(s) in %s' %(m,small))
        for n in small:
            groups[n]=m
        m+=1
    with arcpy.da.UpdateCursor(infc,groupField) as cursor:
        m=0
        for row in cursor:
            row[0]=groups[m]
            cursor.updateRow(row)
            m+=1
except:
    message = "\n*** PYTHON ERRORS *** "; showPyMessage()
    message = "Python Traceback Info: " + traceback.format_tb(sys.exc_info()[2])[0]; showPyMessage()
    message = "Python Error Info: " +  str(sys.exc_type)+ ": " + str(sys.exc_value) + "\n"; showPyMessage()            

Just replace 3 lines at the beginning, starting with infc=... by your shapefile, intermediate output and field to store Group No. Add this field to infc manually. Script will populate this field with unique group numbers. It can be used for dissolve