I am trying to update our street center line information that does not have the "From Street" or "To Street" field populated. All our data has is "On Street". Is there an "easy" way to write a script or is there a set of tools that can be used to help populate this information automatically?
Solved! Go to Solution.
Hi Kevin, the data came through perfectly. Thanks for sharing. I have been in and out of meetings, so not much time dedicated to it yet.
However, first of all I used the Multipart to Singlepart tool to create single part features that will be more easy to process in order to determine the start and end points of a street. It might be necessary to dissolve them again with the unsplit option, since there are streets that should be continuous and are now represented as multiple features..
The input street featureclass does not have M values, so I will have to assume that the streets have been digitized in the correct direction. This is not the case for instance when we look at W. Main St. where both sides of the street are heading West when the lower one should be heading East.
The idea is to use a cursor that loops over each feature in the street featureclass.
For each feature it will select those streets that are within a certain tolerance (have a close look at W. Main St. connecting to N. Maple St. Should this be both N. Maple St. and S. Maple St.?)
In case there are multiple streets as candidates as from or to streets, I will write the first one found (and would you like to have a list of streets?)
There is another challenge, one can find the nearest feature but this is not necessarily an "intersection". It may be a street that continues in the same direction. These type of cases should be disregarded in the process.
I will keep you posted on any results I obtain.
OK, I'm getting some progress:
However, there are quite some challenges in your data. I will get back to this later and post my observations and the script I used.
Cool stuff Xander; I'm looking forward to your solution.
Admittedly I'm nothing of a programmer, but having hacked on this for a few minutes, I don't see it as 'easy'. You'll need to iterate through the from nodes of each street segment, select those streets (sans the active street) that intersect that from node and then determine a method to choose which one, if they are different, goes in the From Street attribute. Repeat the process for the To Nodes.
Maybe Dan_Patterson or dkwiens_edi or xander_bakker can take it from here....
Joe... attachment? or something?
Dan- I was just working through it manually, trying to figure out the steps/procedures.
Hi Kevin Cross and Joe Borgione ,
So here goes... As I mentioned before I used the MultiPart to SinglePart tool to create a featureclass without multipart features. The reason for this is that there can be several line parts in a single feature, and when I extract the firstPoint and lastPoint of the Polyline it will not represent the start and end of the street.
After doing that I noticed that there were some cases (actually quite a number) where the same street continuous with another feature. Dissolve with the unsplit and without creating multiparts could help but this would make you loose some attributes that might be relevant.
So, I ordered the features on the FullName and checked the cases where in an edit session I could merge the feature into one feature. There are some very challenging cases in your data Kevin.
Some of them will simply have not result:
In this case Webster Ln in the middle would have from and to street with the same name and those results are filtered out.
There where some multipart features created in the manual process and if the parts are not in the correct order (and in some case there aren't) it will not find the correct start and end points of the line and because of that not find a from and/or to feature within the defined search tolerance.
Also the fact that there are a substantial number of streets without a FullName makes things a bit more complicated, When a candidate is found that has no name it is filtered out of the results and may result in the intersecting street not to have a from or to street.
There are however, a number of case that should have a result, but do not have any. This would require some serious debugging to understand what is happening in those cases in order to understand what needs to be changed in the code to get results there without the possibility to loose results elsewhere.
After the editing the data manually I ended up with 289 streets and and 212 have a from and/or to street as result. Some of the 77 cases may not have a from or to street, but there are cases, as mentioned before, that should have a result.
For now let's have a look at the code and I will attache the resulting featureclass for you to revise.
#-------------------------------------------------------------------------------
# Name: from_to_street.py
# Purpose:
#
# Author: xbakker
#
# Created: 21/06/2017
#-------------------------------------------------------------------------------
import arcpy
def main():
fc = r'C:\GeoNet\Streets\GeoNet Street Sample.gdb\LebStreetSample_MP2SP'
fld_fullname = 'Fullname'
fld_from = 'From_Street'
fld_to = 'To_Street'
tolerance = 5 # 5 feet distance tolerance for finding intersections
min_angle = 45 # to detect angle intersections
sr = arcpy.Describe(fc).spatialReference
flds = ('OID@', 'SHAPE@', fld_fullname)
dct = {r[0]: [r[1], r[2]] for r in arcpy.da.SearchCursor(fc, flds)}
print "dict filled..."
cnt = 0
dct_res = {} # fill with results
for oid, lst in dct.items():
polyline = lst[0]
full_name = lst[1]
if full_name != ' ':
cnt += 1
if cnt % 50 == 0:
print "Processing:", cnt
from_pnt = polyline.firstPoint
to_pnt = polyline.lastPoint
# get a list of candidates for from and to points
from_oids = SelectFromToCandidates(oid, full_name, from_pnt, dct, tolerance, sr)
to_oids = SelectFromToCandidates(oid, full_name, to_pnt, dct, tolerance, sr)
print
print " - before filter: ", full_name, oid, from_oids, to_oids
if len(from_oids) > 0:
from_oids = FilterListOnAngles(dct, oid, from_oids, min_angle)
if len(to_oids) > 0:
to_oids = FilterListOnAngles(dct, oid, to_oids, min_angle)
print " - after filter: ", full_name, oid, from_oids, to_oids
if len(from_oids) == 0:
from_streets = ''
else:
from_streets = ', '.join([dct[o][1] for o in from_oids])
if len(to_oids) == 0:
to_streets = ''
else:
to_streets = ', '.join([dct[o][1] for o in to_oids])
if from_streets in [' ' , ' , ']:
from_streets = ''
if to_streets in [' ' , ' , ']:
to_streets = ''
print oid, " - From:", from_streets, from_oids
print oid, " - To :", to_streets, to_oids
dct_res[oid] = [oid, from_streets, to_streets]
print "update cursor..."
cnt = 0
updates = 0
flds = ('OID@', fld_from, fld_to)
with arcpy.da.UpdateCursor(fc, flds) as curs:
for row in curs:
cnt += 1
oid = row[0]
if oid in dct_res:
result = dct_res[oid]
curs.updateRow(tuple(result))
updates += 1
if cnt % 50 == 0:
print"processing row:", cnt, " - updates:", updates
def FilterListOnAngles(dct, oid, candidates, angle_tolerance):
polyline = dct[oid][0]
oids_ok = []
for candidate in candidates:
polyline_cand = dct[candidate][0]
if ValidateAngle(polyline, polyline_cand, angle_tolerance, oid):
oids_ok.append(candidate)
return oids_ok
def ValidateAngle(polyline1, polyline2, angle_tolerance, oid):
try:
# get start segment and end segment
start1 = polyline1.segmentAlongLine(0, 1, False)
end1 = polyline1.segmentAlongLine(polyline1.length-1, polyline1.length, False)
# sr = polyline1.spatialReference
# determine distance start segment and end segment to intersecting line
d_start = start1.distanceTo(polyline2)
d_end = end1.distanceTo(polyline2)
if d_start < d_end:
# start is closer to intersecting line
pntg = polyline2.queryPointAndDistance(polyline1.firstPoint, False)[0]
seg1 = start1
else:
# end is closer to intersecting line
pntg = polyline2.queryPointAndDistance(polyline1.lastPoint, False)[0]
seg1 = end1
# get position of projected point on intersecting line
distance_along = polyline2.measureOnLine (pntg, False)
# and extract segment
seg2 = polyline2.segmentAlongLine(max([distance_along -1, 0]),
min([distance_along +1, polyline2.length]), False)
print "seg1.length", seg1.length
print "seg2.length", seg2.length
# compare
compare_angles = [seg1, seg2]
angle_segs = GetAngleSegments(compare_angles)
print "angle_segs 1", angle_segs
while angle_segs < 0:
angle_segs += 180
while angle_segs > 360:
angle_segs -= 360
print "angle_segs 2", angle_segs
if IsBetween(angle_segs, angle_tolerance, 180 - angle_tolerance) or IsBetween(angle_segs, angle_tolerance + 180, 360 - angle_tolerance):
return True
else:
return False
except Exception as e:
print "ValidateAngle ERROR @ OID:", oid
return False
def IsBetween(val, val_min, val_max):
if val >= val_min and val <= val_max:
return True
else:
return False
def GetAngleSegments(segments):
try:
angles = []
for seg in segments:
pntg1 = arcpy.PointGeometry(seg.firstPoint, seg.spatialReference)
pntg2 = arcpy.PointGeometry(seg.lastPoint, seg.spatialReference)
angle = GetAngle(pntg1, pntg2)
angles.append(angle)
#print "angles", angles
angle_between = angles[0] - angles[1]
return angle_between
except:
return None
def GetAngle(pntg1, pntg2):
'''determine angle of line based on start and end points geometries'''
return pntg1.angleAndDistanceTo(pntg2, method='PLANAR')[0]
def SelectFromToCandidates(cur_oid, cur_full_name, pnt, dct, tolerance, sr):
oids = []
pntg = arcpy.PointGeometry(pnt, sr)
for oid, lst in dct.items():
full_name = lst[1]
if oid != cur_oid:
# don't include the current polyline in results
if full_name != cur_full_name:
# don't include street with the same name
polyline = lst[0]
if polyline.distanceTo(pntg) <= tolerance:
oids.append(oid)
return list(set(oids))
if __name__ == '__main__':
main()
So let's see what happens in the script (and to be honest, I'm not very happy with it as it is, since there is a lot of room for enhancement).
This is basically what goes on in the script.
Attached you will find the result.
Hi Kevin Cross and jborgion , did you have a chance to play with the code?
Sorry Xander, haven't had the opportunity, but I'd like to study the code as a lesson. (xander_bakker)
If you need some additional explanation, just let me know.