corners_narrows_workflow.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. # -*- coding: utf-8 -*-
  2. """
  3. This is a workflow to truncate deep and narrow corners and dilate narrow segments
  4. of soil polygons. This workflow is intened to assist in the QA/QC process of SSURGO
  5. Five genveral steps:
  6. 1) Smooth and Generalize features
  7. 2) Identify deep corners and short narrow ends
  8. 3) Truncate features which only share boundary with two MU's
  9. 4) Identify narrow segements of polygons
  10. 5) Dilate narrow segments and update
  11. @author: alexander.stum
  12. Last revised 5/30/18
  13. PrintMsg and errorMsg were written by Steve Peaslee. At the momment these functions
  14. are not utilized. When set-up to be used as an ArcToolbox Script, they will become
  15. useful
  16. """
  17. import arcpy,traceback,sys
  18. #======= User Defined Variables ==========
  19. #The geodatabase with MUPOLYGON and SAPOLYGON
  20. gdb = r'G:\GIS_specialist\Data\Geospatial\soils\FY18\Lubbock\Borden\workspace\Lubbock18_aks051618.gdb'
  21. arcpy.env.workspace = gdb
  22. #The main soil polygon layer
  23. mupolygon = "MUPOLYGON"
  24. #The survey area boundary, NOTE that no features touching boundary will be modified
  25. sapolygon = "SAPOLYGON"
  26. #selection query, specify if the entire survey area is not being evaluated
  27. #otherwise set to None
  28. query = "Update_Name IS NOT NULL"
  29. #query = None
  30. #======= Set Parameters ==========
  31. #Parameter for line smooting of polylines. Subjective, I like 25
  32. smooth_tol = "20 Meters"
  33. #Parameter for the generalization of polylines. Subjective, I like 2.
  34. gen_tol = "2 Meters"
  35. #Half the miniumum width standard, currently national standard is 38 m
  36. min_width = 19
  37. #All deep/narrow corners with Shape_Length less than this parameter will not
  38. #be treated. Leave it to the human
  39. min_len = "300"
  40. #Look ahead parameter, used by centerline function
  41. f = 5
  42. #======= Functions ==========
  43. def PrintMsg(msg, severity=0):
  44. # Adds tool message to the geoprocessor and prints to console window
  45. #Split the message on \n first, so that if it's multiple lines, a GPMessage will be added for each line
  46. try:
  47. for string in msg.split('\n'):
  48. #Add a geoprocessing message (in case this is run as a tool)
  49. if severity == 0:
  50. arcpy.AddMessage(string)
  51. elif severity == 1:
  52. arcpy.AddWarning(string)
  53. elif severity == 2:
  54. arcpy.AddMessage(" ")
  55. arcpy.AddError(string)
  56. except:
  57. pass
  58. ## ===================================================================================
  59. def errorMsg():
  60. try:
  61. tb = sys.exc_info()[2]
  62. tbinfo = traceback.format_tb(tb)[0]
  63. theMsg = tbinfo + "\n" + str(sys.exc_type)+ ": " + str(sys.exc_value)
  64. PrintMsg(theMsg, 2)
  65. except:
  66. PrintMsg("Unhandled error in errorMsg method", 2)
  67. pass
  68. ## ===================================================================================
  69. def centerline(narrows, centerline, extend_to, f=5):
  70. """Calculates centerline between two more or less parrallel polylines (multipart)
  71. which represent the outline of a polygon.
  72. It finds the centerline by starting at the endpoint of a feature. It first
  73. looks at the first five vertices of the opposing part. If the nearest vertex
  74. is the last of these five, it will look at the next five. It does this for
  75. each vertex of the first part. At the end it verifies that the enddpoints of
  76. the second part were involved, if not it similarly finds the nearest vertices
  77. in part one to the uninvolved endbpoints of part 2.
  78. As the two part polyline outline is produced by buffering, the algorithm extends
  79. the centerline to the nearest negative buffer feature to extend the centerline
  80. beyond the inward bowing minicus.
  81. - **Parameters**::
  82. Name: Type: Description:
  83. narrows String Name of the polyline outline feature
  84. centerline String Name of the centerline feature created by function
  85. extend_to String Name of the polygon feature to which centerline will be stretched to
  86. f Integer Look ahead parameter, how many verticies to consider at a time
  87. - **Returns**::
  88. True if successful
  89. .. note:: Requires ArcPy
  90. `
  91. """
  92. s_r = arcpy.Describe(narrows).spatialReference
  93. arcpy.CreateFeatureclass_management(arcpy.env.workspace, centerline, 'POLYLINE',\
  94. spatial_reference= s_r)
  95. def midpoint(pt1,pt2):
  96. x = pt1.X-(pt1.X-pt2.X)/2.0
  97. y = pt1.Y-(pt1.Y-pt2.Y)/2.0
  98. return arcpy.Point(x,y)
  99. sCur = arcpy.da.SearchCursor(narrows,'SHAPE@')
  100. iCur = arcpy.da.InsertCursor(centerline,'SHAPE@')
  101. for poly, in sCur:
  102. if poly and poly.partCount==2:
  103. line = arcpy.Array()
  104. pi = 0 #current position in part 2
  105. p1 = poly.getPart(0)
  106. p2 = [p for p in poly.getPart(1)] #put in list to be able to slice
  107. p1c = p1.count
  108. dlast0 = ((p1[p1c-1].X-p2[0].X)**2+(p1[p1c-1].Y-p2[0].Y)**2)**.5
  109. d00= ((p1[0].X-p2[0].X)**2+(p1[0].Y-p2[0].Y)**2)**.5
  110. if dlast0 < d00:
  111. index= range(p1c-1,-1,-1)
  112. line.add(midpoint(p2[0],p1[p1.count-1]))
  113. else:
  114. index= range(p1c)
  115. line.add(midpoint(p2[0],p1[0]))
  116. for i in index:
  117. pf = f-1 #relative forward position
  118. while pf == f-1: #if the relative forward position is the same as the last relative position, continue looking
  119. d = [((p1[i].X-p.X)**2+(p1[i].Y-p.Y)**2)**.5 \
  120. for p in p2[pi:(pi+f) or None]]
  121. pf = d.index(min(d)) #nearest forward point
  122. pi += pf
  123. line.add(midpoint(p1[i],p2[pi]))
  124. if pi < len(p2)-1: #if last point of part 2 not included, extend
  125. line.add(midpoint(p1[i],p2[-1]))
  126. if (line[0].X==line[1].X) and (line[0].Y==line[1].Y):
  127. line.remove(0) #If first two points are identical, remove one
  128. #split line
  129. if line.count == 2:
  130. mid = midpoint(line[0],line[1])
  131. partA = arcpy.Array([line[0],mid])
  132. partB = arcpy.Array([mid,line[1]])
  133. elif line.count <2: print (line.count)
  134. else:
  135. partA = arcpy.Array([line[i] for i in range(line.count/2+line.count%2)])
  136. partB = arcpy.Array([line[i] for i in range(line.count/2,line.count)])
  137. iCur.insertRow([arcpy.Polyline(partA)])
  138. iCur.insertRow([arcpy.Polyline(partB)])
  139. del sCur, iCur
  140. #Tie centerlines to the nearest non-narrow segment of polygon
  141. arcpy.Near_analysis(centerline, extend_to, "", "LOCATION", "NO_ANGLE")
  142. uCur = arcpy.da.UpdateCursor(centerline,['SHAPE@','NEAR_X','NEAR_Y'])
  143. try:
  144. seg = uCur.next()
  145. while seg:
  146. partA = seg[0].getPart(0)
  147. if ((partA[0].X-seg[1])**2+(partA[0].Y-seg[2])**2)**.5 <= 18:
  148. partA.insert(0,arcpy.Point(seg[1],seg[2]))
  149. uCur.deleteRow()
  150. seg = uCur.next()
  151. partA.extend(seg[0].getPart(0))
  152. if ((partA[partA.count-1].X-seg[1])**2+ \
  153. (partA[partA.count-1].Y-seg[2])**2)**.5 <= 18:
  154. partA.add(arcpy.Point(seg[1],seg[2]))
  155. uCur.updateRow([arcpy.Polyline(partA),seg[1],seg[2]])
  156. seg = uCur.next()
  157. except StopIteration:
  158. return True
  159. ###******************* Part I *****************
  160. #Step 0
  161. arcpy.FeatureToLine_management(mupolygon, "MU_lines", "", "NO_ATTRIBUTES")
  162. arcpy.Dissolve_management("MU_lines", "MU_lines_dis", "",\
  163. "", "SINGLE_PART", "DISSOLVE_LINES")
  164. arcpy.FeatureToPoint_management(mupolygon, "MU_point", "INSIDE")
  165. arcpy.MakeFeatureLayer_management('MU_lines_dis', 'MU_lines_select')
  166. arcpy.Delete_management('MU_lines_dis')
  167. arcpy.Delete_management("MU_lines")
  168. if query:
  169. arcpy.MakeFeatureLayer_management(mupolygon, 'MU_select', query)
  170. arcpy.SelectLayerByLocation_management("MU_lines_select","SHARE_A_LINE_SEGMENT_WITH",\
  171. 'MU_select',"#","NEW_SELECTION")
  172. arcpy.SelectLayerByLocation_management("MU_lines_select","SHARE_A_LINE_SEGMENT_WITH",sapolygon,\
  173. '#',"REMOVE_FROM_SELECTION")
  174. else:
  175. arcpy.SelectLayerByLocation_management("MU_lines_select","SHARE_A_LINE_SEGMENT_WITH",sapolygon,\
  176. '#',"NEW_SELECTION","INVERT")
  177. arcpy.SmoothLine_cartography("MU_lines_select", "MU_lines_gen", "PAEK",\
  178. "25 Meters","FIXED_CLOSED_ENDPOINT", "NO_CHECK")
  179. arcpy.Generalize_edit("MU_lines_gen", "2 Meters")
  180. arcpy.SelectLayerByAttribute_management("MU_lines_select","SWITCH_SELECTION")
  181. arcpy.Merge_management("MU_lines_select;MU_lines_gen", "MU_lines_gen_merge")
  182. arcpy.FeatureToPolygon_management("MU_lines_gen_merge", "MU_gen",\
  183. "", "ATTRIBUTES", "MU_point")
  184. arcpy.Delete_management("MU_lines_gen_merge")
  185. arcpy.Delete_management("MU_point")
  186. arcpy.Delete_management("MU_lines_gen")
  187. if query:
  188. arcpy.MakeFeatureLayer_management('MU_gen', 'MU_gen_select', query)
  189. #Step 1
  190. #Expect to see several warnings of features dissappearing
  191. arcpy.Buffer_analysis("MU_gen_select", "MU_negbuff19", "-"+str(min_width)+" Meters")
  192. #Step 2
  193. arcpy.Buffer_analysis("MU_negbuff19", "MU_rebuff19", str(min_width+.1)+" Meters")
  194. #Step 3
  195. arcpy.Erase_analysis("MU_gen_select", "MU_rebuff19", \
  196. "too_narrow", cluster_tolerance="0.1 Meters")
  197. else:
  198. #Step 1
  199. #Expect to see several warnings of features dissappearing
  200. arcpy.Buffer_analysis("MU_gen", "MU_negbuff19", "-"+str(min_width)+" Meters")
  201. #Step 2
  202. arcpy.Buffer_analysis("MU_negbuff19", "MU_rebuff19",\
  203. str(min_width+.1)+" Meters")
  204. #Step 3
  205. arcpy.Erase_analysis("MU_gen", "MU_rebuff19", "too_narrow", \
  206. cluster_tolerance="0.1 Meters")
  207. arcpy.Delete_management("MU_negbuff19")
  208. arcpy.Delete_management("MU_rebuff19")
  209. #Step 4
  210. arcpy.MultipartToSinglepart_management("too_narrow", "too_narrow_sing")
  211. #Step 5
  212. arcpy.MakeFeatureLayer_management('too_narrow_sing','too_narrow_select',\
  213. 'Shape_Length >=30 AND Shape_Length <=300')
  214. #Step 6
  215. arcpy.Buffer_analysis("too_narrow_select", "corner_buff_4", "0.4 Meters")
  216. #Step 7 Intersect is necessary to produce a polygon county on both sides of narrows to equal 3
  217. #Tolerance set to clean out polygons with 0 area
  218. arcpy.Intersect_analysis("MU_gen #;corner_buff_4 #", \
  219. "corner_inter", "ALL", "0.05 Meters")
  220. #Step 8
  221. arcpy.MultipartToSinglepart_management("corner_inter", "corner_inter_sing")
  222. #Step 9
  223. arcpy.SpatialJoin_analysis("corner_buff_4", "corner_inter_sing",\
  224. "corner_sum","JOIN_ONE_TO_ONE", "KEEP_ALL", "", "INTERSECT")
  225. #Step 10
  226. arcpy.MakeFeatureLayer_management('corner_sum','corner_sum_select',"Join_Count =2")
  227. #Step 11
  228. arcpy.SelectLayerByLocation_management('corner_sum_select',"CROSSED_BY_THE_OUTLINE_OF",\
  229. sapolygon,'#',"NEW_SELECTION",'INVERT')
  230. #Step 12
  231. arcpy.SelectLayerByLocation_management('too_narrow_select',"HAVE_THEIR_CENTER_IN"\
  232. ,'corner_sum_select','#',"NEW_SELECTION")
  233. #Step 13
  234. arcpy.Update_analysis("MU_gen", "too_narrow_select", "MU_cornered", "BORDERS", "0.1 Meters")
  235. #Step 14
  236. arcpy.MakeFeatureLayer_management("MU_cornered","MU_cornered_select")
  237. arcpy.SelectLayerByLocation_management("MU_cornered_select","COMPLETELY_WITHIN",\
  238. 'corner_sum_select')
  239. arcpy.CopyFeatures_management("too_narrow_select",'truncated')
  240. #Step 15 Eliminate
  241. arcpy.Eliminate_management("MU_cornered_select", "MU_decornered", "LENGTH")
  242. arcpy.Delete_management("too_narrow")
  243. arcpy.Delete_management("too_narrow_sing")
  244. arcpy.Delete_management("corner_buff_4")
  245. arcpy.Delete_management("corner_inter")
  246. arcpy.Delete_management("corner_inter_sing")
  247. arcpy.Delete_management("corner_sum")
  248. arcpy.Delete_management("MU_cornered")
  249. ###******************* Part II *****************
  250. #*********************************************************
  251. if query:
  252. arcpy.MakeFeatureLayer_management("MU_decornered", 'MU_decornered_select', query)
  253. #Step 1
  254. arcpy.Buffer_analysis("MU_decornered_select", "MU_2negbuff19","-19 Meters")
  255. #Step 2
  256. arcpy.Buffer_analysis("MU_2negbuff19", "MU_2rebuff19", "19.4 Meters")
  257. #Step 3
  258. arcpy.Erase_analysis('MU_decornered_select', "MU_2rebuff19", \
  259. "too_narrow2", "0.1 Meters")
  260. else:
  261. #Step 1
  262. arcpy.Buffer_analysis("MU_decornered", "MU_2negbuff19","-19 Meters")
  263. #Step 2
  264. arcpy.Buffer_analysis("MU_2negbuff19", "MU_2rebuff19", "19.4 Meters")
  265. #Step 3
  266. arcpy.Erase_analysis("MU_decornered", "MU_2rebuff19",\
  267. "too_narrow2", "0.1 Meters")
  268. arcpy.Delete_management("MU_2rebuff19")
  269. #Step 4
  270. arcpy.MultipartToSinglepart_management("too_narrow2", "too_narrow_sing2")
  271. #Step 5
  272. arcpy.FeatureToLine_management("too_narrow_sing2", "narrow_outlines")
  273. #Step 6
  274. arcpy.Intersect_analysis("narrow_outlines #;MU_decornered #", \
  275. "narrow_outlines_inter", "ALL", "0.5 Meters")
  276. #Step 7
  277. arcpy.MakeFeatureLayer_management("narrow_outlines_inter","narrow_outlines_select",\
  278. "MUSYM <> MUSYM_1")
  279. #Step 8
  280. arcpy.Dissolve_management("narrow_outlines_select", "casings", "FID_too_narrow_sing2")
  281. #Step 9
  282. arcpy.Densify_edit("casings", "DISTANCE", "30 Meters")
  283. #Step 10
  284. centerline('casings','centerlines','MU_2negbuff19')
  285. #Step 11
  286. arcpy.Identity_analysis("centerlines", "MU_decornered", "centerlines_attributed", "ALL")
  287. #Step 12
  288. arcpy.Buffer_analysis("centerlines_attributed","inserts",\
  289. "19 Meters", "FULL", "ROUND", "LIST", "FID_MU_decornered;MUSYM")
  290. #Step 13
  291. arcpy.MakeFeatureLayer_management('inserts','inserts_select')
  292. arcpy.SelectLayerByLocation_management('inserts_select',"CROSSED_BY_THE_OUTLINE_OF",\
  293. sapolygon,'#',"NEW_SELECTION",'INVERT')
  294. arcpy.CopyFeatures_management("inserts_select",'dilations')
  295. #Step 14
  296. arcpy.Update_analysis("MU_decornered", "dilations", "MU_inserted", \
  297. "BORDERS", "0.1 Meters")
  298. #Step 15
  299. arcpy.Dissolve_management("MU_inserted", "MU_dilated", "MUSYM", "", \
  300. "SINGLE_PART", "DISSOLVE_LINES")
  301. arcpy.Delete_management("MU_2negbuff19")
  302. arcpy.Delete_management("too_narrow2")
  303. arcpy.Delete_management("too_narrow_sing2")
  304. arcpy.Delete_management("narrow_outlines")
  305. arcpy.Delete_management("narrow_outlines_inter")
  306. arcpy.Delete_management("centerlines_attributed")
  307. arcpy.Delete_management("inserts")
  308. arcpy.Delete_management("MU_inserted")
  309. ### Densify 31.75 degrees to replace arcs with appropriate density
  310. arcpy.Densify_edit("MU_dilated", "ANGLE", max_angle="31.75")
  311. arcpy.FeatureToPoint_management("MU_decornered","MUpoints","INSIDE")
  312. arcpy.SpatialJoin_analysis("MU_dilated", "MUpoints", "MU_final","JOIN_ONE_TO_ONE")
  313. if query:
  314. arcpy.MakeFeatureLayer_management("MU_final", 'MU_final_select', query)
  315. arcpy.Buffer_analysis("MU_final_select", "MU_3negbuff19","-18.85 Meters")
  316. #Step
  317. arcpy.Buffer_analysis("MU_3negbuff19", "MU_3rebuff19", "19 Meters")
  318. #Step
  319. arcpy.Erase_analysis("MU_final_select", "MU_3rebuff19",\
  320. "too_narrow3", "0.1 Meters")
  321. else:
  322. arcpy.Buffer_analysis("MU_final", "MU_3negbuff19","-18.85 Meters")
  323. #Step
  324. arcpy.Buffer_analysis("MU_3negbuff19", "MU_3rebuff19", "19.25 Meters")
  325. #Step
  326. arcpy.Erase_analysis("MU_final", "MU_3rebuff19",\
  327. "too_narrow3", "0.1 Meters")
  328. #Step
  329. arcpy.MultipartToSinglepart_management("too_narrow3", "Remaining_Narrows")
  330. arcpy.Delete_management("MU_3negbuff19")
  331. arcpy.Delete_management("MU_3rebuff19")
  332. arcpy.Delete_management("too_narrow3")
  333. arcpy.Delete_management("too_narrow3")
  334. #https://www.ian-ko.com/ET_GeoWizards/WhitePapers/gw_smooth_generalize.htm
  335. #http://desktop.arcgis.com/en/arcmap/10.3/analyze/modelbuilder/the-in-memory-workspace.htm#ESRI_SECTION1_1BDEBF62493E489FA7A1CD7E4E951D5A