Iterate selection and calculate field not working

5748
44
09-22-2018 12:04 PM
ThomasColson
MVP Frequent Contributor

With the following, I am trying to select each polygon by its name, select points from another feature class that are within the selected polygon, then calculate a field using the same name that was used to select the polygon (by attributes). A spatial intersect, or any other method that results in creating a new FC as output and having to do joins won't work here, as this is (hopefully) to be an unattended nightly update script. It iterates through the polygons fine, but the result is that ALL points get the name of the last polygon. 

import os
import errno
import arcpy
import sys  
import traceback  
from arcpy import env
update_feature_class = r'C:\Users\tcolson\Documents\ArcGIS\Projects\MyProject\MyProject.gdb\GRSM_RESEARCH_LOCATIONS'
county_feature_class = r'C:\Users\tcolson\Documents\ArcGIS\Projects\MyProject\MyProject.gdb\CountyBoundaries'

with arcpy.da.SearchCursor(county_feature_class,['SHAPE@','NAME']) as cursor:
    for row in cursor:
        print('Selecting '+row[1])
        expression = "NAME = '"+row[1]+"'"
        arcpy.SelectLayerByAttribute_management (county_feature_class, "NEW_SELECTION", expression)
        arcpy.SelectLayerByLocation_management(update_feature_class, "INTERSECT", county_feature_class)
        arcpy.CalculateField_management(update_feature_class, "COUNTY","'"+row[1]+"'", "PYTHON3")
44 Replies
ThomasColson
MVP Frequent Contributor

Here is something interesting: when I try to extend the point-in-polygon script to polygon-centroid-in-polygon (what administrative boundary is this building located in?), if the input polygon intersects the within polygon (building straddles a county boundary), the result is null: 

    with arcpy.da.UpdateCursor(update_feature_class, ['SHAPE@XY', update_name]) as update_cursor:

        # Iterate through points
        for point in update_cursor:
            point_count += 1
            name_list = []
            for name in poly_names:
                comparison_count += 1
                poly = relateDict[name]
                # Check if the current point is within the current polygon
                if point[0].within(poly):  # Returns True or False
                    # Add the polygon name to the list
                    name_list.append(name)
            point[1] = ",".join(name_list)
            # Update the cursor with the new value
            update_cursor.updateRow(point)
    arcpy.AddMessage("Processed {0} Polygon/Polygon or Line comparisons for {1} Polygons and {2} Polygons or Lines".format(comparison_count, poly_count, point_count))
0 Kudos
JoshuaBixby
MVP Esteemed Contributor

SHAPE@XY doesn't return a point geometry, or even a point, it returns a tuple containing coordinates.  In your code snippet as posted, the point[0].within(poly) line should generate an AttributeError since tuples don't contain a within method.  I am guessing the code snippet is abridged and something got left out when you pasted it; otherwise, I don't understand how the code is running at all.

ThomasColson
MVP Frequent Contributor

Hmmmm   How To: Calculate feature centroids  suggests:

"In the following sample, the Data Access Module is used to retrieve the centroid coordinates using the SHAPE@XY token." which I interpret as the polygon centroid. If that is indeed a tuple of many coordinates, how does one get the centroid using the da method?

0 Kudos
DanPatterson_Retired
MVP Emeritus

Polygon—ArcPy classes | ArcGIS Desktop 

using SHAPE@ to get the polygon

then

centroid(Read Only)The true centroid if it is within or on the feature;
 otherwise, the label point is returned. Returns a point object. ‍‍‍

so

cent = row[whatever].centroid # which returns a point object

cent.X and cent.Y if you need the coordinates of the centroid and not the point object itself

JoshuaBixby
MVP Esteemed Contributor

I suggest using Dan's suggestion and returning the geometry itself and then calling the centroid method.

Regarding documentation, the next sentence after the one you quoted is, "These coordinates are then used to build point geometry,...."  As helpful as Esri Support can be, in the hierarchy of documentation they are a step or two below the documentation from Esri Development.  Looking at UpdateCursor—Help | ArcGIS Desktop , the object type is clearly documented:

  • SHAPE@XY —A tuple of the feature's centroid x,y coordinates.
  • SHAPE@TRUECENTROID —A tuple of the feature's true centroid x,y coordinates.
0 Kudos
ThomasColson
MVP Frequent Contributor

Here's the full snippet (which I've attached to a toolbox). Even using SHAPE@ I still get the nulls for polygons that straddle a polygon boundary. Have also tried  SHAPE@ and SHAPE@TRUECENTROID

import arcpy
update_feature_class = arcpy.GetParameterAsText(0)
poly_feature_class = arcpy.GetParameterAsText(1)
update_name = arcpy.GetParameterAsText(2)
poly_name = arcpy.GetParameterAsText(3)

poly_count = 0
relateDict = {}
# Set up search cursor for poly fc
with arcpy.da.SearchCursor(poly_feature_class,[poly_name,'SHAPE@']) as search_cursor:

    # Iterate through polygons
    for polygon in search_cursor:
        if not polygon[0] in relateDict:
            relateDict[polygon[0]] = polygon[1]
        else:
            relateDict[polygon[0]] = relateDict[polygon[0]].union(polygon[1])

poly_names = []
for name in sorted(relateDict):
    poly_names.append(name)
    poly_count += 1

point_count = 0
comparison_count = 0
# Set up update cursor for point fc
with arcpy.da.UpdateCursor(update_feature_class, ['SHAPE@', update_name]) as update_cursor:

    # Iterate through points
    for point in update_cursor:
        point_count += 1
        name_list = []
        for name in poly_names:
            comparison_count += 1
            poly = relateDict[name]
            # Check if the current point is within the current polygon
            if point[0].within(poly):  # Returns True or False
                # Add the polygon name to the list
                name_list.append(name)
        point[1] = ",".join(name_list)
        # Update the cursor with the new value
        update_cursor.updateRow(point)
arcpy.AddMessage("Processed {0} Polygon/Point comparisons for {1} Polygons and {2} Points".format(comparison_count, poly_count, point_count))
0 Kudos
DanPatterson_Retired
MVP Emeritus

How do they straddle a boundary? is that determined via polygon to polygon intersection? (ie polygon intersects polygon …. not polygon within polygon)

If you are comparing the centroid of a straddling polygon, the centroid may not be within the bounds of the intersecting polygon

0 Kudos
ThomasColson
MVP Frequent Contributor

0 Kudos
DanPatterson_Retired
MVP Emeritus

centroid within a distance of.... if you are doing some kind of select by attributes (thread is too long and convoluted now)

0 Kudos
JoshuaBixby
MVP Esteemed Contributor

I agree with Dan that either starting a new question or branching this question to another thread would be good given the length and slight topic change.

What I will say here, though, is that reading through the script for me is made more difficult by naming rows from the cursors with names like "polygon" and "point."  Typically when something is named "polygon," it is a polygon object and not a row containing a polygon object and other attributes.  Maybe it is just me, but I catch myself having to go back and remind myself what exactly "polygon" and "point" are when I see polygon[0] because ArcPy polygon objects do support indexing and slicing.

Beyond variable naming, I am trying to understand why you are creating a poly_count counter when you can just check the length of the poly_names list to get the count.  And I don't see poly_count actually being used anywhere.

Since Python dictionaries are not inherently spatial, looping over them and doing spatial operations on each geometry stored within them can be slow depending on how many polygons are being iterated over, but I haven't followed this thread closely until now, so maybe that discussion already happened.  If so, disregard this last comment.