DOI-USGS / gems-tools-pro

GeMS Tools for ArcGIS Pro
Creative Commons Zero v1.0 Universal
45 stars 15 forks source link

SCRIPT TO SHARE: Attribute Points From Polys #56

Closed alwunder closed 1 year ago

alwunder commented 1 year ago

A Python script I wrote to add data to a point from a polygon via an "in-memory" spatial join (i.e., there are no intermediate outputs or unnecessary fields added to target table). See the script notes for more info. It could probably use some error handling and cleanup, but it works as-is.

# AttributePointsFromPolys.py
# Andrew L. Wunderlich
# andrew.wunderlich@tn.gov
# 6/29/2022

# 2/24/2023  Ported to ArcGIS Pro GeMS toolbox

# This tool was written to accompany and enhance the GeMS tools for geologic map databases.
# Several of the point feature classes specified by the GeMS schema require that the attribute
# "MapUnit" be populated based on the corresponding MapUnitPoly feature the point intersects.
# A Spatial Join can be done to get this information, however the output of that tool must then
# be transferred to the original feature class by way of a table join and field calculation.

# This tool takes four inputs: a target point feature class, a source polygon feature class, and
# once these have been selected, a field to populate in the target and a field to get values from
# in the source. Once the inputs have been selected, the tool does a spatial join (in memory),
# stores the polygon attribute values from the spatial join that correspond to the target point
# feature OIDs, and finally transfers those values to the target point class, all without creating
# an intermediate output or requiring any additional user action.

# This tool DOES NOT check the type of field, the field length, or whether or not the values
# that are attempting to be transferred are valid for the target field. This may be implemented
# in a future version, however, at this time field type mismatches will undoubtedly cause unhandled
# exceptions to occur.

# This tool modifies the TARGET point dataset ONLY.  ALWAYS back up your datasets before using
# a tool that modifies any of the inputs!!

# Add Script setup:

# Name: AttributePointsFromPoly
# Label: Attribute Points From Polygon
# Description: Tool uses an in-memory spatial join to transfer an attribute from a polygon class 
# to the user-specified attribute of a point class which intersects it.
# Store relative paths: YES

# ArcGIS Pro Tool Properties required Parameters:
# The first two parameters are to make Point and Polygon features the only valid types:
#   Target Points - Feature Class; Required; Input; Filter: Feature Type-Point
#   Source Polygons - Feature Class; Required; Input; Filter: Feature type-Polygon
# The third and fourth parameters are populated contingent upon the first two parameters:
#   Target Points Attribute (to be populated) - Field; Required; Input; Dependency: Target_Points
#   Source Polygons Attribute (to be transferred) - Field; Required; Input; Dependency: Source_Polygons

import arcpy
from os import path
import sys
import GeMS_utilityFunctions as guf

# Function to find field names used for debugging purposes
def FindField(fc, myfield):
    fieldList = arcpy.ListFields(fc)
    for field in fieldList:
        if str.lower(str(field.name)) == str.lower(myfield):
            guf.addMsgAndPrint("    " + fc + " contains field name: " + myfield, 0)

# Use these when running from toolbox
fc_target_points = sys.argv[1]
fc_join_polys = sys.argv[2]
fld_target_points = sys.argv[3]
fld_join_polys = sys.argv[4]

# Use these when testing script
# fc_target_points = 'C:\\temp\\AttributePointFromPolyTESTING\\OrientationPointsTEST01.shp'
# fc_join_polys = 'C:\\temp\\AttributePointFromPolyTESTING\\MapUnitPolysTEST01.shp'
# fld_target_points = 'MapUnit'
# fld_join_polys = 'MapUnit'

# Allow overwrite! Must do even when writing to memory
arcpy.env.overwriteOutput = True

# Save the original name of the Source field
fld_orig_join_polys = fld_join_polys

# Create a temporary point feature class to store the spatial join
temp_sj_points = arcpy.CreateFeatureclass_management('in_memory', 'temp_sj', 'Point')

# Do the spatial join between the target points and the polygons
arcpy.SpatialJoin_analysis(fc_target_points, fc_join_polys, temp_sj_points, 'JOIN_ONE_TO_ONE', 'KEEP_ALL', None,
                           'INTERSECT', '0')

# For debug purposes, check the list of fields in the temp_sj_points
# testFields = arcpy.ListFields(temp_sj_points)
# guf.addMsgAndPrint('\rFields present in the temporary spatial join output (points):', 0)
# for field in testFields:
#     guf.addMsgAndPrint('    {0} is a type of {1} with a length of {2}'
#           .format(field.name, field.type, field.length), 0)

# Set the current workspace
arcpy.env.workspace = path.dirname(fc_target_points)
guf.addMsgAndPrint('\rCurrent workspace: ' + arcpy.env.workspace, 0)

# Retrieve the field list from the spatial join
fieldList = arcpy.ListFields(fc_target_points)
for field in fieldList:
    if str.lower(str(field.name)) == str.lower(fld_join_polys): # Check if the field name already exists
        guf.addMsgAndPrint('\rTarget point features ' + path.split(fc_target_points)[1] + ' already has a field named \"' +
                       fld_join_polys + '\"...', 0)
        # Add an '_1' to the end of the name if the field already exists
        # MUST do this when from and to attribute field names are the same
        fld_join_polys = fld_join_polys + '_1'
        guf.addMsgAndPrint('\r    Spatial join will rename Source field to \"' +
                       fld_join_polys + '\" in the temporary spatial join point features...', 0)

# Get the field list from the spatial join. The polygon OID will always be called 'TARGET_FID' in the join output
sourceFieldsList = ['TARGET_FID', fld_join_polys]
guf.addMsgAndPrint('\rSpatial join of ' + path.split(fc_target_points)[1] + ' and ' + path.split(fc_join_polys)[1] +
               ' complete...\r\n    Polygon ObjectIDs and \"' + fld_orig_join_polys + '\" values transferred to fields'
               ' created by the join [Source Polygon OID, attribute]: ' + str(sourceFieldsList), 0)

# Get the field list of the target points
updateFieldsList = ['OID@', fld_target_points]
guf.addMsgAndPrint('\rTarget point feature fields to match and transfer to are [Target Point OID, attribute]: ' + str(updateFieldsList) + '\r\n', 0)

# Populate the value array from the fields in the temporary join feature class
# based on the 'TARGET_FID' field generated by the join and the values present
# in the user-specified source field that was populated by the join.
valueDict = {r[0]: (r[1:]) for r in arcpy.da.SearchCursor(temp_sj_points, sourceFieldsList)}

# USED FOR DEBUG: Value dump to see what's in there... output actual values to user below during iteration
# guf.addMsgAndPrint('\rValues: ' + str(valueDict), 0)

guf.addMsgAndPrint('Begin transferring attributes...\r', 0)
# Go through the target features and match up the OID with the TARGET_FID
with arcpy.da.UpdateCursor(fc_target_points, updateFieldsList) as updateRows:
    for updateRow in updateRows:
        keyValue = updateRow[0]
        if keyValue in valueDict:
            for n in range(1, len(sourceFieldsList)):
                if valueDict[keyValue][n - 1] is None:
                    # Prevents an error in non-nullable text fields
                    # Could the None keyword be used here to pass along the NULL?
                    updateRow[n] = ''
                    guf.addMsgAndPrint('    ' + fld_orig_join_polys + ' value is NULL for ' +
                                   path.split(fc_target_points)[1] + ' OID ' + str(keyValue) + ' (Point does not '
                                   'intersect any polygons); Set to non-NULL \"\" instead...', 0)
                else:
                    # Should use a try except here to catch type mismatches...
                    updateRow[n] = valueDict[keyValue][n - 1]
                    guf.addMsgAndPrint('    ' + fld_orig_join_polys + ' value transferred to ' +
                                   path.split(fc_target_points)[1] + ' OID ' + str(keyValue) + '; ' + fld_target_points
                                   + ' = ' + valueDict[keyValue][n - 1], 0)
            updateRows.updateRow(updateRow)

# Delete the valueDict array and the temporary join
del valueDict
arcpy.Delete_management(temp_sj_points)
guf.addMsgAndPrint(' ', 0)