Python add in- update two fields in one feature based on same fields of another

8060
43
Jump to solution
04-14-2014 11:10 AM
AndrewMosley
Emerging Contributor
I'm totally new to making add ins and quite novice at python itself but I have a script that executes perfectly as a script tool or simply in the python window of ArcMap. I want to make this a quick button click so I looked into making an add-in toolbar with a button.

Just to get the hang of making the addin i made a print "Hello World!" that works as expected.

However, it seems something is going wrong with my code when put into the add-in. I can only assume I'm missing some syntax due to my inexperience with python.

The situation here is I have a percels layer and an address points layer, which both contain a PIN and TMS number. I simply want to have the function select the address points within the parcel geometry, find the value of PIN and TMS from the parcel layer's attribute table, and update the same fields in the address points' attribute table. Again, the script I've written executes perfectly inside the python console in ArcMap but not as an add-in. Thanks for any hints you all might have!

So here's what I've got:

import arcpy import pythonaddins  class BlueBird(object):     """Implementation for BlueBird.button (Button)"""     def __init__(self):         self.enabled = True         self.checked = False     def onClick(self): arcpy.SelectLayerByLocation_management("Address Points","COMPLETELY_WITHIN","Parcels")  #insert if statement checking that only one parcel is selected to prevent selecting all address points  with arcpy.da.SearchCursor("Parcels",'PIN') as cursor:  for row in cursor:   storedPIN = row[0] with arcpy.da.UpdateCursor("Address Points",'PIN') as cursor:  for row in cursor:   row[0] = storedPIN   cursor.updateRow(row)  del cursor  with arcpy.da.SearchCursor("Parcels",'TMS') as cursor:  for row in cursor:   storedTMS = row[0] with arcpy.da.UpdateCursor("Address Points",'TMS') as cursor:  for row in cursor:   row[0] = storedTMS   cursor.updateRow(row)  del cursor, storedPIN, storedTMS
Tags (2)
0 Kudos
43 Replies
RichardFairhurst
MVP Alum
The code you posted appears to include a fair amount of pseudo code that does not actually work.  I need to remove all of that first to get back to code that works and is clean for the situation where only 1 parcel is selected and then rebuild it from there.

Test this code out.  It deals with just the single parcel situation.  The selection has to be specifically tested for just the parcel layer and not against any old selections in the map.

I renamed the row and cursor variables to be more specific to make it clearer which is being used in the event thing get more complex and intermingled later.

My understanding is that you don't need to del the cursor when you use the with syntax.  The help says "Search and update cursors also support With statements. The advantage of using a With statement is that it will guarantee close and release of database locks and reset iteration regardless of whether the cursor completed successfully or an exception occurred."

Let's debug this code (I have not run it) and make sure you understand what I did to make it work where only one parcel is selected before we proceed to deal with the case of 2 or more parcels (where you have to include the Select By Location within the loop).

import arcpy
import pythonaddins

class BlueBird(object):
    """Implementation for BlueBird.button (Button)"""
    def __init__(self):
        self.enabled = True
        self.checked = False
    def onClick(self):
        #Get the current map document and the first data frame.
        mxd = arcpy.mapping.MapDocument('current')
        df = arcpy.mapping.ListDataFrames(mxd)[0]
        for lyr in arcpy.mapping.ListLayers(mxd, "", df):
            if lyr.name == "Address Points":
                addressLayer = lyr
            if lyr.name == "Parcels":
                parcelsLayer = lyr

        #make sure user has 1 parcel selected, else end
        parcelCount = int(arcpy.GetCount_management(parcelsLayer).getOutput(0))
        if parcelCount == 1:
              
            #select by location outside of loop to save time
            arcpy.SelectLayerByLocation_management(addressLayer,"COMPLETELY_WITHIN",parcelsLayer)          

            with arcpy.da.SearchCursor(parcelsLayer,['PIN', 'TMS']) as parcelCursor:
                for parcelRow in parcelCursor:
                    PIN = parcelRow[0]
                    TMS = parcelRow[1]
 
            with arcpy.da.UpdateCursor(addressLayer,['PIN', 'TMS']) as addressCursor:
                for addressRow in addressCursor:
                    addressRow[0] = PIN
                    addressRow[1] = TMS
                    addressCursor.updateRow(addressRow)

            del PIN, TMS

        else:
            MessageBox("Please select just one parcel")
0 Kudos
RichardFairhurst
MVP Alum
Given that I have provided substantial help so far, please check out this link about the forum point and answer marking system.

Also consider the possibility that your tool is essentially replicating the effect of the Spatial Join tool.  With that tool and a standard join of the tool output back to your original points you may be able to complete everything with a single script made up of about 4 to 6 tools in ModelBuilder.
0 Kudos
AndrewMosley
Emerging Contributor
Unfortunately joins are out due to permissions we have set on the respective layers, or you're right- I would have simply run that tool. Trying to make an inefficient situation and little less so.

My knowledge is lacking in terms of what arcpy modules are available, so I thank you for your patience.

I do understand what made the last iteration of code work, as I can see your last suggestion should as well, given the GetCount command.

You said that the SelectByLocation must be included within the loop- I assume this statement (along with lack of search results) means there is no arcpy command to remove the previously iterated through row from selection?



P.S. I checked out the up voting thread. Thanks again!
0 Kudos
PaulDavidson1
Frequent Contributor

There's a nice reference to arcpy and a detailed reference to arcpy.mapping that can be found at:

http://ianbroad.com/arcpy-resources/

And of course the arcpy help...  But Ian's synopsis is handy to have a list in one place.

I'm not sure how current it is but it is dated 2013.

0 Kudos
RichardFairhurst
MVP Alum
Unfortunately joins are out due to permissions we have set on the respective layers, or you're right- I would have simply run that tool. Trying to make an inefficient situation and little less so.

My knowledge is lacking in terms of what arcpy modules are available, so I thank you for your patience.

I do understand what made the last iteration of code work, as I can see your last suggestion should as well, given the GetCount command.

You said that the SelectByLocation must be included within the loop- I assume this statement (along with lack of search results) means there is no arcpy command to remove the previously iterated through row from selection?



P.S. I checked out the up voting thread. Thanks again!


Well I understand why that limitation would make this tool a worthwhile endeavor.  And thank you for the up votes.

As far as the SelectByLocation, my recollection is that you need two copies of the parcelsLayer (one can exist in memory through the MakeFeatureLayer_managment tool).  The map copy holds the user's original selection of parcels, and the second in memory layer selects one parcel feature at a time to operate on the addresses through the SelectByLocation tool.  I will lay it out descriptively to see if you follow my logic and try to just assist you with the actual code as you run into issues.  You will learn more that way.

Outside of all the loops create a new layer for the parcel feature class to create an in memory parcel layer in addition to the two layers you have read from the map.

With your parcel original loop you still get the two field values as previously written.  However, you also need to get the ObjectID or unique Parcel Number field value with the other two fields in the outer parcel loop.  Add that additional field to the field name list in the braces.

Use the ObjectID or unique Parcel Number value from the parcel currently being read to perform a SelectByAttribute or cursor selection against the in Memory parcel layer to select that one parcel.

The SelectByLocation follows and is now inside of the outer parcel loop.  It uses the AddressLayer and the in memory parcel layer to do the SelectByLocation.

The address loops must now be indented to be embedded inside of the outer parcel loop (the main reason I renamed the rows and cursors).  The indents for the cursor creation will be at the same level as the SelectByAttributes operation above.

The inner address loop now will only work on a selection from one parcel (from the in memory parcel layer) and apply the correct attributes that were read for just that one parcel.

After all addresses are processed for the current parcel the process returns to the outer parcel loop and iterates to the next parcel.  The whole process repeats until each parcel is read individually and does the selections and attribute transfers to the real associated addresses.

Give yourself a message to let you know all parcels are done when all of the cursor loops have been completed.  The code finishes.

I hope this outlines the approach you need to take clearly enough for you to have a go at it.
0 Kudos
RichardFairhurst
MVP Alum
I am afraid I could never come work at your office.  Joins account for a huge portion of my work in GIS and I would curl up and die if I could not use them.
AndrewMosley
Emerging Contributor
So based on your logic here's what I came up with:

import arcpy
import pythonaddins

class BlueBird(object):
    """Implementation for BlueBird.button (Button)"""
    def __init__(self):
        self.enabled = True
        self.checked = False
    def onClick(self):
        #Get the current map document and the first data frame.
        mxd = arcpy.mapping.MapDocument('current')
        df = arcpy.mapping.ListDataFrames(mxd)[0]

        #Check to make sure user had at least one parcel selected.
        parcelsCount = int(arcpy.GetCount_management("Parcels").getOutput(0))
        if 0 < parcelsCount < 2:

            #Select Address Points within selected parcel
            arcpy.SelectLayerByLocation_management("Address Points", "HAVE_THEIR_CENTER_IN", "Parcels")

            with arcpy.da.SearchCursor("Parcels", ['PIN','TMS']) as singleCursor:
                for singleRow in singleCursor:
                    PIN = singleRow[0]
                    TMS = singleRow[1]

            with arcpy.da.UpdateCursor("Address Points", ['PIN', 'TMS']) as singleCursor:
                for singleRow in singleCursor:
                    singleRow[0] = PIN
                    singleRow[1] = TMS
                    singleCursor.updateRow(singleRow)

        elif 1 < parcelsCount < 1000:

            #Create in memory parcel layer
            selectedParcels = arcpy.MakeFeatureLayer_management("Parcels", "selectedParcels")

            for lyr in arcpy.mapping.ListLayers(mxd, "", df):
                if lyr.name == "Address Points":
                    addressLayer = lyr
                if lyr.name == "Parcels":
                    parcelsLayer = lyr

            with arcpy.da.SearchCursor("Parcels", ['PIN', 'TMS', 'OBJECTID']) as parcelCursor:
                for parcelRow in parcelCursor:
                    PIN = parcelRow[0]
                    TMS = parcelRow[1]
                    OID = parcelRow[2]

                    OID = str(OID)
                    arcpy.SelectLayerByAttribute_management("selectedParcels", "NEW_SELECTION", "OBJECTID= "+OID)

                    #Select Address Points by location, within selected Parcels.
                    arcpy.SelectLayerByLocation_management("Address Points","HAVE_THEIR_CENTER_IN","selectedParcels")

                    with arcpy.da.UpdateCursor("Address Points", ['PIN', 'TMS']) as addressCursor:
                        for addressRow in addressCursor:
                            addressRow[0] = PIN
                            addressRow[1] = TMS
                            addressCursor.updateRow(addressRow)

                    del PIN, TMS

            arcpy.Delete_management("selectedParcels")

        else:
            print('Please select a valid number of parcels.')


I decided to make an elif statement for when only a single parcel is selected to save time and RAM from the in memory layer.
~3 seconds to run for 1 parcel
~20 seconds to run 2+

It's working quite well now, though I'm sure there's a better way to do the where clause in SelectLayerByAttribute.

Thanks again!
0 Kudos
AndrewMosley
Emerging Contributor
Well it looks like I spoke too soon.
I had been testing on a backup database.
When tried out on the live DB it throws this error:

Line 53
ExecuteError: ERROR 999999: Error executing function.
The requested operation is invalid on a closed state [gisdev.GISDBA.AddressPoints][STATE_ID = 3149277]
Failed to execute (SelectLayerByLocation).

We are working on versions of a DB.sde, however as nothing in the code is a literal path I did not expect that to have much of an impact. Any thoughts?
0 Kudos
RichardFairhurst
MVP Alum
Well it looks like I spoke too soon.
I had been testing on a backup database.
When tried out on the live DB it throws this error:

Line 53
ExecuteError: ERROR 999999: Error executing function.
The requested operation is invalid on a closed state [gisdev.GISDBA.AddressPoints][STATE_ID = 3149277]
Failed to execute (SelectLayerByLocation).

We are working on versions of a DB.sde, however as nothing in the code is a literal path I did not expect that to have much of an impact. Any thoughts?


SDE make a total difference in the code you have to write to do any database updates.  Outside of SDE you don't need to start an edit session to run an update cursor.  Inside SDE you absolutely do have to start an edit session.  I don't think the add-on sees the map edit session any more than it sees the map without specific connections to the active editor session.  I am not an expert on the differences of editing in an SDE database via Python so start by searching the help (like I would have to) on Python Editor SDE.

My IT just broke all of my GIS hyperlinks so I am a bit busy at the moment.
0 Kudos
RichardFairhurst
MVP Alum
Here is the code you need to edit a versioned SDE feature class.  You may want to enable undo/redo in the line that reads edit.startEditing(False, True) by changing it to edit.startEditing(True, True).

import arcpy
import os
import pythonaddins

class BlueBird(object):
    """Implementation for BlueBird.button (Button)"""
    def __init__(self):
        self.enabled = True
        self.checked = False
    def onClick(self):
        # Assign string literal layer names to variables to make them easier to change with just one line of code.
        # Good practice for template code that you may not want to do a search and replace to adapt it for new layers.
        parcels = "Parcels"
        addressPoints = "Address Points"

        #Get the current map document and the first data frame.
        mxd = arcpy.mapping.MapDocument('current')
        df = arcpy.mapping.ListDataFrames(mxd)[0]

        #Check to make sure user had at least one parcel selected.
        parcelsCount = int(arcpy.GetCount_management(parcels).getOutput(0))
        if 0 < parcelsCount < 2:
           
            lyr = arcpy.mapping.ListLayers(mxd, addressPoints, df)[0]
 
            workspace = lyr.workspacePath

            # Start an edit session. Must provide the workspace.
            edit = arcpy.da.Editor(workspace)

            # Edit session is started without an undo/redo stack for versioned data
            #  (for second argument, use False for unversioned data)
            edit.startEditing(False, True)

            # Start an edit operation
            edit.startOperation()

                #Select Address Points within selected parcel
                arcpy.SelectLayerByLocation_management(addressPoints, "HAVE_THEIR_CENTER_IN", parcels)

                with arcpy.da.SearchCursor(parcels, ['PIN','TMS']) as singleCursor:
                    for singleRow in singleCursor:
                        PIN = singleRow[0]
                        TMS = singleRow[1]

                with arcpy.da.UpdateCursor(addressPoints, ['PIN', 'TMS']) as singleCursor:
                    for singleRow in singleCursor:
                        singleRow[0] = PIN
                        singleRow[1] = TMS
                        singleCursor.updateRow(singleRow)

            # Stop the edit operation.
            edit.stopOperation()

            print('Parcel address update is complete.')

        elif 1 < parcelsCount < 1000:

            #Create in memory parcel layer
            selectedParcels = arcpy.MakeFeatureLayer_management(parcels, "selectedParcels")

            for lyr in arcpy.mapping.ListLayers(mxd, "", df):
                if lyr.name == addressPoints:
                    addressLayer = lyr
                if lyr.name == parcels:
                    parcelsLayer = lyr

            workspace = addressLayer.workspacePath

            # Start an edit session. Must provide the workspace.
            edit = arcpy.da.Editor(workspace)

            # Edit session is started without an undo/redo stack for versioned data
            #  (for second argument, use False for unversioned data)
            edit.startEditing(False, True)

            # Start an edit operation
            edit.startOperation()

                with arcpy.da.SearchCursor(parcels, ['PIN', 'TMS', 'OBJECTID']) as parcelCursor:
                    for parcelRow in parcelCursor:
                        PIN = parcelRow[0]
                        TMS = parcelRow[1]
                        OID = parcelRow[2]

                        OID = str(OID)
                        arcpy.SelectLayerByAttribute_management("selectedParcels", "NEW_SELECTION", "OBJECTID= "+OID)

                        #Select Address Points by location, within selected Parcels.
                        arcpy.SelectLayerByLocation_management(addressPoints,"HAVE_THEIR_CENTER_IN","selectedParcels")

                        with arcpy.da.UpdateCursor(addressPoints, ['PIN', 'TMS']) as addressCursor:
                            for addressRow in addressCursor:
                                addressRow[0] = PIN
                                addressRow[1] = TMS
                                addressCursor.updateRow(addressRow)

                        del PIN, TMS

            # Stop the edit operation.
            edit.stopOperation()

            arcpy.Delete_management("selectedParcels")
 
            print('Parcel address update is complete.')

        else:
            print('Please select a valid number of parcels.')
0 Kudos