ArcPy – Copying Parallel Lines in ArcMap with Offset

arcgis-desktoparcmaparcpyoffsetparallel lines

This question has been marked as a duplicate multiple times now. Please read the entire question before flagging it as a duplicate. The previous "duplicates" for questions about copy parallel do not produce the same result as copy parallel tool.

I am looking for an arcpy process that EXACTLY replicates the copy parallel editor tool in ArcGIS 10.3. This is not the same thing as simply offsetting lines with arcpy. That is simple, but it does not replicate the copy parallel tool.

I am using ArcMap with a STANDARD licence. Advanced licence functions won't work as a solution (buffer with flat endcaps)

As an example, I have run the script from this answer https://gis.stackexchange.com/a/181545/56638. I used an offset of x = 20 and y = -20
which produces the red lines in the image below (yellow line is original line).

Then I used the copy parallel tool and set the offset to 20. That produced the green lines in the image. These lines are exactly 20m from the original line.

The problem is that when offsetting with the arcpy solution, the angle of the line is not considered. Therefore, the red lines (produced using arcpy method) are more than 20m from the original line. If you want the offset line to be a certain distance from the original line, you would need to know the angle of the line and calculate the distances to offset x and y based on trig. I am struggling to come up with a solution for this.

enter image description here

Another example with the arcpy solution, with offset set to x = 20 and y = 0. This demonstrates the problems that bends cause and highlights the need to take into account the angle or bearing of the original line.

enter image description here

This is the code that I have tried so far:

tmpwrkspace = r'C:\Users\jasbal\Documents\ArcGIS\Default.gdb'
tempFeatureClass = tmpwrkspace + "\temp_rline"
tempFeatureClass2 = tmpwrkspace + "\temp_lline"
fc_line = r'C:\Users\jasbal\Documents\ArcGIS\Default.gdb\RWP_Rev0_4'
if arcpy.Exists(tempFeatureClass):
    arcpy.Delete_management(tempFeatureClass)
arcpy.CopyFeatures_management(fc_line, tempFeatureClass)
if arcpy.Exists(tempFeatureClass2):
    arcpy.Delete_management(tempFeatureClass2)
arcpy.CopyFeatures_management(fc_line, tempFeatureClass2)
xOffset = 20
yOffset = -20
xOffset2 = -20
yOffset2 = 20
with arcpy.da.UpdateCursor(tempFeatureClass, ["SHAPE@XY"]) as cursor:
    for row in cursor:
        cursor.updateRow([[row[0][0] + xOffset, row[0][1] + yOffset]])
del cursor
with arcpy.da.UpdateCursor(tempFeatureClass2, ["SHAPE@XY"]) as cursor:
    for row in cursor:
        cursor.updateRow([[row[0][0] + xOffset2, row[0][1] + yOffset2]])
del cursor

My idea is to calculate the angle of the line and to use that in a trig function to calculate x and y offset based on the distance I want to be from the original line.

Something like this:

distance = arcpy.GetParameterAsText(0)
angle = "Angle Field" obtained with search cursor
xOffset = distance/cos(angle)
yOffset = distance * tan(angle)

I am not sure if I have my trig correct. I need some advice on this part.

I am not looking for a complete code snippet for copy parallel. The only part I need to figure out is how to calculate the offsets correctly for a set distance from the original line based on the line angle.

Best Answer

After trying a few different methods to achieve a solution, I have settled on a modified version of the code found here: https://gis.stackexchange.com/a/229386/56638

Here is the full code from FelixIP's answer:

import arcpy, math
infc=r'..\SCRARCH\clone.shp'

def CopyParallel(plyP,sLength):
    part=plyP.getPart(0)
    lArray=arcpy.Array();rArray=arcpy.Array()
    for ptX in part:
        dL=plyP.measureOnLine(ptX)
        ptX0=plyP.positionAlongLine (dL-0.01).firstPoint
        ptX1=plyP.positionAlongLine (dL+0.01).firstPoint
        dX=float(ptX1.X)-float(ptX0.X)
        dY=float(ptX1.Y)-float(ptX0.Y)
        lenV=math.hypot(dX,dY)
        sX=-dY*sLength/lenV;sY=dX*sLength/lenV
        leftP=arcpy.Point(ptX.X+sX,ptX.Y+sY)
        lArray.add(leftP)
        rightP=arcpy.Point(ptX.X-sX, ptX.Y-sY)
        rArray.add(rightP)
    array = arcpy.Array([lArray, rArray])
    section=arcpy.Polyline(array)
    return section


with arcpy.da.UpdateCursor(infc,("Shape@","Width")) as cursor:
    for shp,w in cursor:
        twoLines=CopyParallel(shp,w)
        cursor.updateRow((twoLines,w))

This code create two parallel lines, one on either side of the original line. I only need to offset my line in one direction, so I modified the code as follows:

Make sure you run this on a copy of your data. It does not preserve the original input lines.

Import modules and assign input FC:

import arcpy, math
infc=r'C:\temp\test.shp'

Create two functions, one for left, and one for right side of line:

Left side:

def CopyParallelL(plyP,sLength):
    part=plyP.getPart(0)
    lArray=arcpy.Array()
    for ptX in part:
        dL=plyP.measureOnLine(ptX)
        ptX0=plyP.positionAlongLine (dL-0.01).firstPoint
        ptX1=plyP.positionAlongLine (dL+0.01).firstPoint
        dX=float(ptX1.X)-float(ptX0.X)
        dY=float(ptX1.Y)-float(ptX0.Y)
        lenV=math.hypot(dX,dY)
        sX=-dY*sLength/lenV;sY=dX*sLength/lenV
        leftP=arcpy.Point(ptX.X+sX,ptX.Y+sY)
        lArray.add(leftP)
    array = arcpy.Array([lArray])
    section=arcpy.Polyline(array)
    return section

Right:

def CopyParallelR(plyP,sLength):
    part=plyP.getPart(0)
    rArray=arcpy.Array()
    for ptX in part:
        dL=plyP.measureOnLine(ptX)
        ptX0=plyP.positionAlongLine (dL-0.01).firstPoint
        ptX1=plyP.positionAlongLine (dL+0.01).firstPoint
        dX=float(ptX1.X)-float(ptX0.X)
        dY=float(ptX1.Y)-float(ptX0.Y)
        lenV=math.hypot(dX,dY)
        sX=-dY*sLength/lenV;sY=dX*sLength/lenV
        rightP=arcpy.Point(ptX.X-sX, ptX.Y-sY)
        rArray.add(rightP)
    array = arcpy.Array([rArray])
    section=arcpy.Polyline(array)
    return section

Run the update cursor and call the appropriate function:

with arcpy.da.UpdateCursor(infc,("Shape@","Width")) as cursor:
    for shp,w in cursor:
        LeftLine=CopyParallelL(shp,w)
        cursor.updateRow((LeftLine,w))

Here is an example output using the left side function: enter image description here

Related Question