While researching the answer I came across a potential solution from Tom Dilts. He created a Tool in Model Builder which will create perpendicular station lines from a a series of points along a line. His Tool is available to download from here:
Station Lines Arc10.0.
Citation for the use of the tool is:
Preferred citation: Dilts, T.E. (2015) Station Lines for ArcGIS 10.1. University of Nevada Reno. Available at: http://www.arcgis.com/home/item.html?id=3f05ef15203e4a3ca88fc1d92347512b
The setup of the Tool is a little tricky, and you have to manually place the inputs and outputs into Model Builder. I have taken the model and created a *.PYT which simplifies the entry of a lot of the inputs. The code for the Python Toolbox is below. However, there are some limitations, the main being the input and outputs of the Tool need to be in a Directory...NOT a Geodatabase. The Tool will throw all sorts of errors if even one input / output is linked to a Geodatabase.
Also needs an Advanced License
The way I would see your workflow:
- Convert the vertices of your Trail or River to Points - these points are the input of the tool
- Run the tool using the converted vertices as the point file and whatever line file you generated them from as the input line. Choose a distance that the Station Lines will be created to (in map units - try use a distance longer than the maximum distance between the river and trail).
- The output will be a series of lines which run perpendicular to the input line and point. Intersect these lines with the Trail or River (depending on what you used as the input) to get the point at which the corresponding point is perpendicular. When you run the intersect choose the output feature type of the intersect as "Point".
- You can use a spatial join to evaluate which points connect to which lines and intersect at which point.
Below please find the examples of the Inputs and Output.
Below please see how the tool appears (1 - the original tool from the link and the circle...as the PYT appears)
The output:
Intersecting, green dots are the corresponding points:
Finally the Python Toolbox Code - Copy into a text file and saves as a *.PYT and "Add a Toolbox" in ArcMap. Just note, this code is as is, I took it straight from the source, I have no claim to it and full rights and credit goes to the author. I simply made it a Toolbox so I could use it easier.
'''
Name: Create Perpendicular Lines
Purpose:
Author: Tom Dilts
Created: 24 Jan2018
Copyright: None
Licence:
Adapted from the Toolbox Authored by Tom Dilts
Preferred citation: Dilts, T.E. (2015) Station Lines for ArcGIS 10.1. University of Nevada Reno. Available at: http://www.arcgis.com/home/item.html?id=3f05ef15203e4a3ca88fc1d92347512b
'''
# import modules
import arcpy
'''Do not change the name of this class. It will break the toolbox.'''
class Toolbox(object):
def __init__(self):
'''Define toolbox properties (the toolbox anme is the .pyt filename).'''
self.label = "Create Station Lines"
self.alias = ""
# List of tool classes associated with this toolbox
self.tools = [Tool]
class Tool(object):
def __init__(self):
'''Define the tool (tool name is the class name).'''
self.label = "Station Lines"
self.description = "Creates station lines (perpendicular lines) at user-specified points along a polyline. Station lines are a useful first step for hydraulic analysis of rivers"
self.canRunInBackground = False
def getParameterInfo(self):
'''parameter definitions for GUI'''
param0 = arcpy.Parameter(
displayName = "Input Line File",
name = "Input Line",
datatype = "GPFeatureLayer",
parameterType = "Required",
direction = "Input")
param0.filter.list = ['Polyline']
param0.value = ""
param1 = arcpy.Parameter(
displayName = "Input Converted Vertices",
name = "Input Points",
datatype = "GPFeatureLayer",
parameterType = "Required",
direction = "Input")
param1.filter.list = ['Point']
param1.value = ""
param2 = arcpy.Parameter(
displayName = "Input Transect Distance",
name = "Input Transect Distance",
datatype = "GPDouble",
parameterType = "Required",
direction = "Input")
param2.value = '1000'
param3 = arcpy.Parameter(
displayName = "Output File",
name = "Output",
datatype = "DEShapefile",
parameterType = "Required",
direction = "OutPut")
param4 = arcpy.Parameter(
displayName = "Working Directory",
name = "Working Directory",
datatype = "DEFolder",
parameterType = "Required",
direction = "Input")
params = [param0, param1, param2, param3, param4]
return params
def isLicensed(self):
'''Set whether tool is licensed to execute.'''
return True
def updateParameters(self, parameters):
'''Modify the values and properties of parameters before internal
validation is performed. This method is called whenever a parameter
has been changed.'''
return
def updateMessages(self, parameters):
'''Modify the messages created by internal validation for each tool
parameter. This method is called after internal validation.'''
return
def execute(self, parameters, messages):
'''The source code of the tool.'''
Input_Line_Shapefile = parameters[0].valueAsText
Input_Points = parameters[1].valueAsText
Trasect_Distance = parameters[2].valueAsText
Output_Station_Lines= parameters[3].valueAsText
WorkingDir = parameters[4].valueAsText
arcpy.AddMessage (Input_Line_Shapefile + " Added to Inputs")
arcpy.AddMessage (Input_Points + " Added to Inputs")
arcpy.AddMessage (Trasect_Distance + " Added to Inputs")
arcpy.AddMessage (Output_Station_Lines + " Added to Inputs")
Output_Features = Input_Points
points = Output_Features
Trial_FeatureVerticesToPoint = points
splitline_shp = WorkingDir + "splitline.shp"
Output_Feature_Class = WorkingDir + "azimuth.shp"
copy_Select = Output_Feature_Class
copy_Select__2_ = copy_Select
splitline_shp__6_ = copy_Select__2_
splitline_shp__5_ = splitline_shp__6_
spatjoin_shp = WorkingDir + "spatjoin.shp"
Left = WorkingDir + "spatjoin_BearingDistanceToLi.shp"
Right = WorkingDir + "spatjoin_BearingDistanceToLi1.shp"
mergedata = WorkingDir + "merge.shp"
# Process: Add XY Coordinates
arcpy.AddXY_management(Input_Points)
arcpy.AddMessage ("XY Added")
# Process: Add Field (3)
arcpy.AddField_management(Output_Features, "Dist", "LONG", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Calculate Field (3)
arcpy.CalculateField_management(points, "Dist", Trasect_Distance, "VB", "")
arcpy.AddMessage ("Calcs Done")
# Process: Split Line At Vertices
arcpy.SplitLine_management(Input_Line_Shapefile, splitline_shp)
arcpy.AddMessage ("Line split")
#Azimuth added
Input_Line_Features = splitline_shp
Output_Feature_Class = WorkingDir + "azimuth.shp"
# Local variables:
Converted_Graphics_7 = Input_Line_Features
Converted_Graphics_7__2_ = Converted_Graphics_7
Converted_Graphics_7__3_ = Converted_Graphics_7__2_
Converted_Graphics_7__4_ = Converted_Graphics_7__3_
Converted_Graphics_7__6_ = Converted_Graphics_7__4_
Converted_Graphics_7__9_ = Converted_Graphics_7__6_
Converted_Graphics_7__5_ = Converted_Graphics_7__9_
Converted_Graphics_7__8_ = Converted_Graphics_7__5_
Converted_Graphics_7__10_ = Converted_Graphics_7__8_
Converted_Graphics_7__7_ = Converted_Graphics_7__10_
Converted_Graphics_7__13_ = Converted_Graphics_7__7_
Converted_Graphics_7__12_ = Converted_Graphics_7__13_
Converted_Graphics_7__15_ = Converted_Graphics_7__12_
Converted_Graphics_7__11_ = Converted_Graphics_7__15_
Converted_Graphics_7__17_ = Converted_Graphics_7__11_
Converted_Graphics_7__16_ = Converted_Graphics_7__17_
Converted_Graphics_7__19_ = Converted_Graphics_7__16_
Output_Layer = "copy_Layer"
Export_Output_3_Layer = Output_Layer
Export_Output_3_Layer__3_ = Export_Output_3_Layer
Export_Output_3_Layer__4_ = Export_Output_3_Layer__3_
Export_Output_3_Layer__2_ = Export_Output_3_Layer__4_
Export_Output_3_Layer__6_ = Export_Output_3_Layer__2_
Export_Output_3_Layer__5_ = Export_Output_3_Layer__6_
Export_Output_3_Layer__7_ = Export_Output_3_Layer__5_
Export_Output_3_Layer__9_ = Export_Output_3_Layer__7_
Export_Output_3_Layer__11_ = Export_Output_3_Layer__9_
Export_Output_3_Layer__8_ = Export_Output_3_Layer__11_
Output_stuff = Export_Output_3_Layer__8_
copy_Layer = Output_stuff
# Process: Add Field
arcpy.AddField_management(Input_Line_Features, "StartX", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Add Field (2)
arcpy.AddField_management(Converted_Graphics_7, "StartY", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Add Field (3)
arcpy.AddField_management(Converted_Graphics_7__2_, "EndX", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Add Field (4)
arcpy.AddField_management(Converted_Graphics_7__3_, "EndY", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Calculate Field
arcpy.CalculateField_management(Converted_Graphics_7__4_, "StartX", "!Shape!.firstpoint.X", "PYTHON_9.3", "")
# Process: Calculate Field (2)
arcpy.CalculateField_management(Converted_Graphics_7__6_, "StartY", "!shape.firstpoint.y!", "PYTHON_9.3", "")
# Process: Calculate Field (3)
arcpy.CalculateField_management(Converted_Graphics_7__9_, "EndX", "!shape.lastpoint.x!", "PYTHON_9.3", "")
# Process: Calculate Field (4)
arcpy.CalculateField_management(Converted_Graphics_7__5_, "EndY", "!shape.lastpoint.y!", "PYTHON_9.3", "")
# Process: Add Field (5)
arcpy.AddField_management(Converted_Graphics_7__8_, "rad", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Calculate Field (5)
arcpy.CalculateField_management(Converted_Graphics_7__10_, "rad", "Atn(( [StartX] - [EndX]) / ( [StartY] - [EndY] ))", "VB", "")
# Process: Add Field (6)
arcpy.AddField_management(Converted_Graphics_7__7_, "Angle", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Calculate Field (6)
arcpy.CalculateField_management(Converted_Graphics_7__13_, "Angle", "[rad] * (180 / 3.14159265)", "VB", "")
# Process: Add Field (7)
arcpy.AddField_management(Converted_Graphics_7__12_, "EastWest", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Calculate Field (7)
arcpy.CalculateField_management(Converted_Graphics_7__15_, "EastWest", "[StartX] - [EndX]", "VB", "")
# Process: Add Field (8)
arcpy.AddField_management(Converted_Graphics_7__11_, "NorthSouth", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Calculate Field (8)
arcpy.CalculateField_management(Converted_Graphics_7__17_, "NorthSouth", "[StartY] - [EndY]", "VB", "")
# Process: Add Field (9)
arcpy.AddField_management(Converted_Graphics_7__16_, "Azimuth", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Make Feature Layer
arcpy.MakeFeatureLayer_management(Converted_Graphics_7__19_, Output_Layer, "", "", "FID FID VISIBLE NONE;Shape Shape VISIBLE NONE;OBJECTID OBJECTID VISIBLE NONE;ARCID ARCID VISIBLE NONE;GRID_CODE GRID_CODE VISIBLE NONE;FROM_NODE FROM_NODE VISIBLE NONE;TO_NODE TO_NODE VISIBLE NONE;ET_ID ET_ID VISIBLE NONE;Shape_Le_1 Shape_Le_1 VISIBLE NONE;Year_ Year_ VISIBLE NONE;Shape_Leng Shape_Leng VISIBLE NONE;Name Name VISIBLE NONE;Azimuth Azimuth VISIBLE NONE;StartX StartX VISIBLE NONE;StartY StartY VISIBLE NONE;EndX EndX VISIBLE NONE;EndY EndY VISIBLE NONE;rad rad VISIBLE NONE;Angle Angle VISIBLE NONE;EastWest EastWest VISIBLE NONE;NorthSouth NorthSouth VISIBLE NONE")
# Process: Select Layer By Attribute
arcpy.SelectLayerByAttribute_management(Output_Layer, "NEW_SELECTION", "\"NorthSouth\" > 0")
# Process: Calculate Field (9)
arcpy.CalculateField_management(Export_Output_3_Layer, "Azimuth", "[Angle] + 180", "VB", "")
# Process: Select Layer By Attribute (2)
arcpy.SelectLayerByAttribute_management(Export_Output_3_Layer__3_, "NEW_SELECTION", "\"NorthSouth\" < 0 and \"EastWest\" > 0")
# Process: Calculate Field (10)
arcpy.CalculateField_management(Export_Output_3_Layer__4_, "Azimuth", "[Angle] + 360", "VB", "")
# Process: Select Layer By Attribute (3)
arcpy.SelectLayerByAttribute_management(Export_Output_3_Layer__2_, "NEW_SELECTION", "\"NorthSouth\" < 0 and \"EastWest\" < 0")
# Process: Calculate Field (11)
arcpy.CalculateField_management(Export_Output_3_Layer__6_, "Azimuth", "[Angle]", "VB", "")
# Process: Select Layer By Attribute (4)
arcpy.SelectLayerByAttribute_management(Export_Output_3_Layer__5_, "NEW_SELECTION", "\"NorthSouth\" = 0 and \"EastWest\" > 0")
# Process: Calculate Field (12)
arcpy.CalculateField_management(Export_Output_3_Layer__7_, "Azimuth", "270", "VB", "")
# Process: Select Layer By Attribute (5)
arcpy.SelectLayerByAttribute_management(Export_Output_3_Layer__9_, "NEW_SELECTION", "\"NorthSouth\" = 0 and \"EastWest\" < 0")
# Process: Calculate Field (13)
arcpy.CalculateField_management(Export_Output_3_Layer__11_, "Azimuth", "90", "VB", "")
# Process: Delete Field
arcpy.DeleteField_management(Export_Output_3_Layer__8_, "StartX;StartY;EndX;EndY;rad;Angle;EastWest;NorthSouth")
# Process: Select Layer By Attribute (6)
arcpy.SelectLayerByAttribute_management(Output_stuff, "NEW_SELECTION", "\"Azimuth\" < 365")
# Process: Select
arcpy.Select_analysis(copy_Layer, Output_Feature_Class, "Azimuth <= 365")
arcpy.AddMessage ("Azimuth calculated")
#back
# Process: Add Field
arcpy.AddField_management(Output_Feature_Class, "left", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Add Field (2)
arcpy.AddField_management(copy_Select, "right", "DOUBLE", "", "", "", "", "NON_NULLABLE", "NON_REQUIRED", "")
# Process: Calculate Field
arcpy.CalculateField_management(copy_Select__2_, "right", "[Azimuth] + 90", "VB", "")
# Process: Calculate Field (2)
arcpy.CalculateField_management(splitline_shp__6_, "left", "[Azimuth] - 90", "VB", "")
# Process: Spatial Join
arcpy.SpatialJoin_analysis(Trial_FeatureVerticesToPoint, splitline_shp__5_, spatjoin_shp, "JOIN_ONE_TO_ONE", "KEEP_ALL", "OBJECTID \"OBJECTID\" true true false 10 Long 0 10 ,First,#,vertex,OBJECTID,-1,-1;POINT_X \"POINT_X\" true true false 0 Double 0 0 ,First,#,vertex,POINT_X,-1,-1;POINT_Y \"POINT_Y\" true true false 0 Double 0 0 ,First,#,vertex,POINT_Y,-1,-1;POINT_Z \"POINT_Z\" true true false 0 Double 0 0 ,First,#,vertex,POINT_Z,-1,-1;POINT_M \"POINT_M\" true true false 0 Double 0 0 ,First,#,vertex,POINT_M,-1,-1;Dist \"Dist\" true true false 0 Long 0 0 ,First,#,vertex,Dist,-1,-1;Id \"Id\" true true false 10 Long 0 10 ,First,#,C:\\temp\\azimuth.shp,Id,-1,-1;NEAR_FID \"NEAR_FID\" true true false 10 Long 0 10 ,First,#,C:\\temp\\azimuth.shp,NEAR_FID,-1,-1;NEAR_DIST \"NEAR_DIST\" true true false 19 Double 0 0 ,First,#,C:\\temp\\azimuth.shp,NEAR_DIST,-1,-1;NEAR_X \"NEAR_X\" true true false 19 Double 0 0 ,First,#,C:\\temp\\azimuth.shp,NEAR_X,-1,-1;NEAR_Y \"NEAR_Y\" true true false 19 Double 0 0 ,First,#,C:\\temp\\azimuth.shp,NEAR_Y,-1,-1;Azimuth \"Azimuth\" true true false 19 Double 0 0 ,First,#,C:\\temp\\azimuth.shp,Azimuth,-1,-1;left \"left\" true true false 19 Double 0 0 ,First,#,C:\\temp\\azimuth.shp,left,-1,-1;right \"right\" true true false 19 Double 0 0 ,First,#,C:\\temp\\azimuth.shp,right,-1,-1", "INTERSECT", "", "")
# Process: Bearing Distance To Line
arcpy.BearingDistanceToLine_management(spatjoin_shp, Left, "POINT_X", "POINT_Y", "Dist", "9001", "left", "9102", "0", "TARGET_FID", "PROJCS['Hartebeeshoek94_Lo31',GEOGCS['GCS_Hartebeesthoek_1994',DATUM['D_Hartebeesthoek_1994',SPHEROID['WGS_1984',6378137.0,298.257223563]],PRIMEM['Greenwich',0.0],UNIT['Degree',0.0174532925199433]],PROJECTION['Transverse_Mercator'],PARAMETER['False_Easting',0.0],PARAMETER['False_Northing',0.0],PARAMETER['Central_Meridian',31.0],PARAMETER['Scale_Factor',1.0],PARAMETER['Latitude_Of_Origin',0.0],UNIT['Meter',1.0]];-5623200 -10002100 10000;-100000 10000;-100000 10000;0.001;0.001;0.001;IsHighPrecision")
# Process: Bearing Distance To Line (2)
arcpy.BearingDistanceToLine_management(spatjoin_shp, Right, "POINT_X", "POINT_Y", "Dist", "9001", "right", "9102", "0", "TARGET_FID", "PROJCS['Hartebeeshoek94_Lo31',GEOGCS['GCS_Hartebeesthoek_1994',DATUM['D_Hartebeesthoek_1994',SPHEROID['WGS_1984',6378137.0,298.257223563]],PRIMEM['Greenwich',0.0],UNIT['Degree',0.0174532925199433]],PROJECTION['Transverse_Mercator'],PARAMETER['False_Easting',0.0],PARAMETER['False_Northing',0.0],PARAMETER['Central_Meridian',31.0],PARAMETER['Scale_Factor',1.0],PARAMETER['Latitude_Of_Origin',0.0],UNIT['Meter',1.0]];-5623200 -10002100 10000;-100000 10000;-100000 10000;0.001;0.001;0.001;IsHighPrecision")
# Process: Merge
# Process: Merge
arcpy.Merge_management(WorkingDir + "spatjoin_BearingDistanceToLi.shp" + ";" + WorkingDir + "spatjoin_BearingDistanceToLi1.shp", mergedata, "POINT_X \"POINT_X\" true true false 0 Double 0 0 ,First,#,C:\\temp\\DDP\\spatjoin_BearingDistanceToLi.shp,POINT_X,-1,-1,C:\\temp\\DDP\\spatjoin_BearingDistanceToLi1.shp,POINT_X,-1,-1;POINT_Y \"POINT_Y\" true true false 0 Double 0 0 ,First,#,C:\\temp\\DDP\\spatjoin_BearingDistanceToLi.shp,POINT_Y,-1,-1,C:\\temp\\DDP\\spatjoin_BearingDistanceToLi1.shp,POINT_Y,-1,-1;Dist \"Dist\" true true false 0 Double 0 0 ,First,#,C:\\temp\\DDP\\spatjoin_BearingDistanceToLi.shp,Dist,-1,-1,C:\\temp\\DDP\\spatjoin_BearingDistanceToLi1.shp,Dist,-1,-1;left \"left\" true true false 0 Double 0 0 ,First,#,C:\\temp\\DDP\\spatjoin_BearingDistanceToLi.shp,left,-1,-1;TARGET_FID \"TARGET_FID\" true true false 0 Double 0 0 ,First,#,C:\\temp\\DDP\\spatjoin_BearingDistanceToLi.shp,TARGET_FID,-1,-1,C:\\temp\\DDP\\spatjoin_BearingDistanceToLi1.shp,TARGET_FID,-1,-1;right \"right\" true true false 0 Double 0 0 ,First,#,C:\\temp\\DDP\\spatjoin_BearingDistanceToLi1.shp,right,-1,-1")
# Process: Dissolve
arcpy.Dissolve_management(mergedata, Output_Station_Lines, "TARGET_FID", "", "MULTI_PART", "DISSOLVE_LINES")
return