Table of Contents
Introduction
Spatial Thoughts on LinkedIn posted the following challenge…Can you take a dataset of a river centerline and varying lengths of line transects and make them exactly the same length? using any tool or platform of choice. This post uses ArcPy with ArcGIS Pro and can be used with a Basic license. Here is a link to the posting in LinkedIn.
Import the ArcPy module
ArcPy is a Python site package that enables automation, analysis, and management of geographic data within the ArcGIS software environment. Import math from the standard Python library to help with some calculations.
import arcpy
import math
User inputs
Our tool will require three user inputs; transects (linear Feature Layer), centerline (linear Feature Layer), and line_length (Double)
## input transect linear features
transects = arcpy.GetParameter(0)
## input centerline
centerline = arcpy.GetParameter(1)
## the length to make each transect
line_length = arcpy.GetParameter(2)
Functions
We need two function to help us with calculations. The first is to get the angle between two points…
## http://wikicode.wikidot.com/get-angle-of-line-between-two-points
def getAngle(pt1, pt2):
"""
Get the angle between two points
Args:
pt1 an arcpy Point object
pt2 an arcpy Point object
Returns:
A float representing the angle between pt1 and pt2
"""
## pt2 X value - pt1 X value
x_diff = pt2.X - pt1.X
## pt2 Y value - pt1 Y value
y_diff = pt2.Y - pt1.Y
return math.degrees(math.atan2(y_diff, x_diff))
… and the second is to get the start and end points of the new line to create based on the new line length.
## get a point based on bearing and distance from another point
## in our case the other point is the intersection of the transect and centerline
def getLineEnds(pt, bearing, dist):
"""
Get the start/end point of the new transect line
Args:
pt1 an arpy Point object
bearing float representing an angle between two points on the line
dist distance along the bearing where we want to locate the point
Returns:
A tuple representing x,y coords of start/end of new transect line
"""
## convert to radians
bearing = math.radians(bearing)
## some mathsy stuff that I dont have a clue about
x = pt.X + dist * math.cos(bearing)
y = pt.Y + dist * math.sin(bearing)
return (x, y)
Required Objects
We have a couple of required objects to aid with the workflow. We get half of the line input to create the same line length either side of the transect-centerline intersection. ArcPy doesnt seem to like using an Update Cursor on a Feature Layer from a GeoPackage so we need to get the full filepath to the layer in the GeoPackage.
## get half the intended full length of the transect as we will construct a
## line from the intersection of the orginal transect the same length either
## side of the centerline
half_length = line_length / 2
## arcpy doesnt like taking in a GeoPackage layer from the Content and
## using the Update Cursor so we get the full filepath.
transects = arcpy.da.Describe(transects)["catalogPath"]
Get centreline geometry
Next, we get the geometry of the centerline as an ArcPy Polyline object. We need this to find where each transect intersects.
centerline = [row[0] for row in arcpy.da.SearchCursor(centerline,"SHAPE@")][0]
The Main Event: Resizing the Transects
And now for the main event, we iterate through each transect and get the current geometry, we get the intersection point between the transect and the centerline as this should always be the midpoint of the line. Armed with the first, end, and intersect points of the transect we can construct a new line using the bearings of the original and the new line length from the user input. We use the Update Cursor to update each transects geometry as we iterate.
with arcpy.da.UpdateCursor(transects, "SHAPE@") as cursor:
## for each transect
for row in cursor:
## check if straight line, i.e. only contains two points
if row[0].pointCount == 2:
## get start, end, and cnterline intersction point
start_point = row[0].firstPoint
end_point = row[0].lastPoint
intersect_point = row[0].intersect(centerline, 1).firstPoint
####################################################################
## GET NEW LINE START POINT ########################################
intersect_start_angle = getAngle(intersect_point, start_point)
new_start = getLineEnds(intersect_point, intersect_start_angle, half_length)
####################################################################
## GET NEW LINE END POINT ##########################################
intersect_end_angle = getAngle(intersect_point, end_point)
new_end = getLineEnds(intersect_point, intersect_end_angle, half_length)
####################################################################
## CREATE NEW LINE GEOMETRY ########################################
new_line = arcpy.Polyline(arcpy.Array(arcpy.Point(*coords) for coords in [new_start, new_end]))
####################################################################
## UPDATE TRANSECT GEOMETRY ########################################
row[0] = new_line
cursor.updateRow(row)
Leverage ArcPy for geospatial data management workflows within ArcGIS Pro. Learn the fundamentals of utilising ArcGIS Pro geoprocessing tools with ArcPy for data management, conversions, and analysis. This course is packed with amazing content that will help you discover the power of automating tasks within the ArcGIS Pro environment. Take your ArcPy skills from beginner to snake charmer. A little code goes a long way, a little ArcPy provides you with an in-demand skill. Sign up now for our highly rated course.
Create the tool in ArcGIS Pro
Save your script and open up ArcGIS Pro. Right-click on your toolbox/toolset of choice and select New > Script. The New Script window will appear. In the General tab set Name to resizeTransects, Label to Resize Centerline Transects , and the Description as per below or any description you feel is apt.
Set the Input Transect Linear Features with a Data Type of Feature Layer and set the Filter to Polyline geometry only and do the same for the Input Centerline Feature parameter. Set the Line Length with a Data Type of Double.
In the Execution tab, click the folder icon in the top-right corner and add your saved Python script.
You can download the tool and other custom tools over on this page. This tool is in the Custom Tools on a Basic License with ArcPy section.
Here is a before…
…and after with shorter transects to 200m in length.
All the code in one place
You can find the entire code workflow below with links to important components in the documentation that were used.
import arcpy
import math
################################################################################
## Esri Documentation
## https://pro.arcgis.com/en/pro-app/3.2/arcpy/functions/getparameter.htm
## https://pro.arcgis.com/en/pro-app/3.2/arcpy/functions/describe.htm
## https://pro.arcgis.com/en/pro-app/3.2/arcpy/classes/point.htm
## https://pro.arcgis.com/en/pro-app/3.2/arcpy/classes/multipoint.htm
## https://pro.arcgis.com/en/pro-app/3.2/arcpy/classes/polyline.htm
## https://pro.arcgis.com/en/pro-app/3.2/arcpy/classes/array.htm
## https://pro.arcgis.com/en/pro-app/3.2/arcpy/data-access/searchcursor-class.htm
## https://pro.arcgis.com/en/pro-app/3.2/arcpy/data-access/updatecursor-class.htm
##
## ArcGIS Pro Version 3.2.0
##
#################################################################################
#################################################################################
## USER INPUTS
## input transect linear features
transects = arcpy.GetParameter(0)
## input centerline
centerline = arcpy.GetParameter(1)
## the length to make each transect
line_length = arcpy.GetParameter(2)
################################################################################
## FUNCTIONS ###################################################################
## http://wikicode.wikidot.com/get-angle-of-line-between-two-points
def getAngle(pt1, pt2):
"""
Get the angle between two points
Args:
pt1 an arcpy Point object
pt2 an arcpy Point object
Returns:
A float representing the angle between pt1 and pt2
"""
## pt2 X value - pt1 X value
x_diff = pt2.X - pt1.X
## pt2 Y value - pt1 Y value
y_diff = pt2.Y - pt1.Y
return math.degrees(math.atan2(y_diff, x_diff))
## get a point based on bearing and distance from another point
## in our case the other point is the intersection of the transect and centerline
def getLineEnds(pt, bearing, dist):
"""
Get the start/end point of the new transect line
Args:
pt1 an arpy Point object
bearing float representing an angle between two points on the line
dist distance along the bearing where we want to locate the point
Returns:
A tuple representing x,y coords of start/end of new transect line
"""
## convert to radians
bearing = math.radians(bearing)
## some mathsy stuff that I dont have a clue about
x = pt.X + dist * math.cos(bearing)
y = pt.Y + dist * math.sin(bearing)
return (x, y)
################################################################################
## REQUIRED OBJECTS ############################################################
## get half the intended full length of the transect as we will construct a
## line from the intersection of the orginal transect the same length either
## side of the centerline
half_length = line_length / 2
## arcpy doesnt like taking in a GeoPackage layer from the Content and
## using the Update Cursor so we get the full filepath.
transects = arcpy.da.Describe(transects)["catalogPath"]
################################################################################
## GET THE CENTERLINE AS AN ARCPY POLYLINE OBJECT ##############################
centerline = [row[0] for row in arcpy.da.SearchCursor(centerline,"SHAPE@")][0]
################################################################################
## CREATE NEW GEOMETRY FOR EACH TRANSECT ######################################
with arcpy.da.UpdateCursor(transects, "SHAPE@") as cursor:
## for each transect
for row in cursor:
## check if straight line, i.e. only contains two points
if row[0].pointCount == 2:
## get start, end, and cnterline intersction point
start_point = row[0].firstPoint
end_point = row[0].lastPoint
intersect_point = row[0].intersect(centerline, 1).firstPoint
####################################################################
## GET NEW LINE START POINT ########################################
intersect_start_angle = getAngle(intersect_point, start_point)
new_start = getLineEnds(intersect_point, intersect_start_angle, half_length)
####################################################################
## GET NEW LINE END POINT ##########################################
intersect_end_angle = getAngle(intersect_point, end_point)
new_end = getLineEnds(intersect_point, intersect_end_angle, half_length)
####################################################################
## CREATE NEW LINE GEOMETRY ########################################
new_line = arcpy.Polyline(arcpy.Array(arcpy.Point(*coords) for coords in [new_start, new_end]))
####################################################################
## UPDATE TRANSECT GEOMETRY ########################################
row[0] = new_line
cursor.updateRow(row)
################################################################################