1

I have a shapefile with polygons and want to write the Python script to eliminate collinear vertices in a polygon. I found a solution for the line feature class, but not for polygon: Deleting collinear vertex / vertices using ArcMap.

import arcpy
 
#----------------------------------------------------------------------
def are_collinear(p1, p2, p3, tolerance=0.5):
 """return True if 3 points are collinear.
 tolerance value will decide whether lines are collinear; may need
 to adjust it based on the XY tolerance value used for feature class"""
 x1, y1 = p1[0], p1[1]
 x2, y2 = p2[0], p2[1]
 x3, y3 = p3[0], p3[1]
 res = x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)
 if -tolerance <= res <= tolerance:
 return True
#----------------------------------------------------------------------
def get_redundant_vertices(vertices):
 """get redundant vertices from a line shape vertices"""
 indexes_of_vertices_to_remove = []
 start_idx, middle_index, end_index = 0, 1, 2
 for i in range(len(vertices)):
 p1, p2, p3 = vertices[start_idx:end_index + 1]
 if are_collinear(p1, p2, p3):
 indexes_of_vertices_to_remove.append(middle_index)
 start_idx += 1
 middle_index += 1
 end_index += 1
 if end_index == len(vertices):
 break
 return indexes_of_vertices_to_remove
#----------------------------------------------------------------------
def clean_geometries(fc, densify_curves=False):
 """clean polyline features in the fc removing redundant vertices"""
 in_sr = arcpy.Describe(fc).spatialReference.factoryCode
 with arcpy.da.UpdateCursor(fc, ['SHAPE@', 'OID@']) as ucur:
 for row in ucur:
 print "OBJECTID", row[1]
 cleaned_parts = []
 shape = row[0]
 if 'curvePaths' in shape.JSON:
 if densify_curves:
 shape = shape.densify('DISTANCE', 1, 1)
 else:
 continue
 for part in range(shape.partCount):
 vertices = [(p.X, p.Y) for p in shape.getPart(part)]
 if len(vertices) < 3: #polyline's part consists of 2 vertices
 continue
 vertices_to_remove = get_redundant_vertices(vertices)
 vertices_to_keep = [
 val for idx, val in enumerate(vertices)
 if idx not in vertices_to_remove
 ]
 cleaned_part_as_array = arcpy.Array(
 [arcpy.Point(*coords) for coords in vertices_to_keep])
 cleaned_parts.append(cleaned_part_as_array)
 if cleaned_parts:
 cleaned_shape = arcpy.Polyline(
 arcpy.Array(cleaned_parts), in_sr)
 row[0] = cleaned_shape
 ucur.updateRow(row)
if __name__ == '__main__':
 fc = r'C:\GIS\Temp\ArcGISHomeFolder\Default.gdb\_SimpleRoads'
 #r'C:\GIS\Temp\ArcGISHomeFolder\empty.gdb\roads'
 clean_geometries(fc, densify_curves=False)

If I use this code for a polygon FC it will delete my polygons. I am new to Python and I don't know how to fix it in ArcPy.

Could you give me some tips or ideas how to achieve it? Unwanted vertices are marked with circles

asked Feb 14, 2021 at 13:11
2
  • Any Ramer-Douglas-Peucker implementation with a really tight threshold ought to accomplish this. For ArcGIS, with polygons, that could be arcpy.SimplifyPolygon_cartography. Commented Feb 14, 2021 at 14:01
  • @Vince actually I tried RDP algorithm. If I use threshold like 0.5 it deletes some of the needed vertices, if I set 0.001 it deletes nothing. E.g.: s = df.simplify(tolerance=0.001, preserve_topology=True) Commented Feb 14, 2021 at 14:10

1 Answer 1

3

I believe you only need to edit a few lines of code.

The lines I have edited/added are marked with ### at the end of the line. The key method is boundary(), it returns the polygon as a polyline and feeds that into the rest of the existing logic. The update cursor also now updates the geometry as a polygon.

So this code now expects the input to be a polygon dataset, not tested it with multi-parts either.

import arcpy
#----------------------------------------------------------------------
def are_collinear(p1, p2, p3, tolerance=0.5):
 """return True if 3 points are collinear.
 tolerance value will decide whether lines are collinear; may need
 to adjust it based on the XY tolerance value used for feature class"""
 x1, y1 = p1[0], p1[1]
 x2, y2 = p2[0], p2[1]
 x3, y3 = p3[0], p3[1]
 res = x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)
 if -tolerance <= res <= tolerance:
 return True
#----------------------------------------------------------------------
def get_redundant_vertices(vertices):
 """get redundant vertices from a line shape vertices"""
 indexes_of_vertices_to_remove = []
 start_idx, middle_index, end_index = 0, 1, 2
 for i in range(len(vertices)):
 p1, p2, p3 = vertices[start_idx:end_index + 1]
 if are_collinear(p1, p2, p3):
 indexes_of_vertices_to_remove.append(middle_index)
 start_idx += 1
 middle_index += 1
 end_index += 1
 if end_index == len(vertices):
 break
 return indexes_of_vertices_to_remove
#----------------------------------------------------------------------
def clean_geometries(fc, densify_curves=False):
 """clean polyline features in the fc removing redundant vertices"""
 in_sr = arcpy.Describe(fc).spatialReference.factoryCode
 with arcpy.da.UpdateCursor(fc, ['SHAPE@', 'OID@']) as ucur:
 for row in ucur:
 print "OBJECTID", row[1]
 cleaned_parts = []
 poly = row[0] ###
 shape = poly.boundary() ###
 if 'curvePaths' in shape.JSON:
 if densify_curves:
 shape = shape.densify('DISTANCE', 1, 1)
 else:
 continue
 for part in range(shape.partCount):
 vertices = [(p.X, p.Y) for p in shape.getPart(part)]
 if len(vertices) < 3: #polyline's part consists of 2 vertices
 continue
 vertices_to_remove = get_redundant_vertices(vertices)
 vertices_to_keep = [
 val for idx, val in enumerate(vertices)
 if idx not in vertices_to_remove
 ]
 cleaned_part_as_array = arcpy.Array(
 [arcpy.Point(*coords) for coords in vertices_to_keep])
 cleaned_parts.append(cleaned_part_as_array)
 if cleaned_parts:
 cleaned_shape = arcpy.Polygon(arcpy.Array(cleaned_parts), in_sr) ###
 row[0] = cleaned_shape
 ucur.updateRow(row)
if __name__ == '__main__':
 fc = r'C:\Scratch\fGDB_Scratch.gdb\testpoly_1'
 #r'C:\GIS\Temp\ArcGISHomeFolder\empty.gdb\roads'
 clean_geometries(fc, densify_curves=False)
 print("done!")
answered Feb 14, 2021 at 19:06
6
  • Thank you! I tried out and it deleted all my polygons :( Commented Feb 15, 2021 at 12:28
  • I think you need to share some of your data so others can look at it? Maybe its to do with the tolerance value, there is an assumption that this data is NOT WGS84? Commented Feb 15, 2021 at 21:17
  • I guess, it is WGS84. Thank you so much! I attached the link in the bottom of my post. Commented Feb 16, 2021 at 16:58
  • I think the issue is with the coordinate system, yours being in WGS84 and the tolerance setting for the function are_collinear. 0.5 would be 0.5 decimal degrees, not sure what that would be in metres. So you need to re-project your data to be using such code, Looking on SpatialReference website there is this coordinate system for Kazan? Commented Feb 16, 2021 at 18:47
  • Oh, right! Now I've got your point! I will try with the right one, thank you so much! :) Commented Feb 16, 2021 at 19:11

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.