Python add-in - get list of selected features in order selected

5437
10
05-06-2014 09:18 AM
Zeke
by
Regular Contributor III
I'm trying to create a python addin that will fill a list with the objectids of features in the order they're selected. This is so I can then add incremental numbers to them in that order. Trying to automate lot numbering, basically. The order of the objectids themselves doesn't necessarily correspond to lot order, so can't just sort them. The idea is that the user would select the features, then click a button to add the lot numbers.

I can add numbers using Chris Fox's UniqueIDSort script, but there's no existing field to sort on. Main problem is retrieving the objectid when the script hasn't run yet. Tried using onEditorSelectioChanged() under Extensions, which presumably could just listen for the event and build the list, but don't see documentation on how to check if a feature has been selected or unselected. Any help much appreciated.
Tags (2)
0 Kudos
10 Replies
T__WayneWhitley
Frequent Contributor
...interesting.  If I haven't oversimplified, if sounds to me that you need to listen for the selection change, keep track of a list that records the OIDs then list them in that noted order in a form to fill in the lot numbers?  Otherwise, I think (if you need to maintain the selection set), you'd need to 'remember' in some way what's in the set before the selection changed, compare the 'before' and 'after' state of all the OIDs to get the added OID - in this way, you'd be doing this for every single feature selection (in other words, on selection change).

So, an extension of this idea to apply it to your add-in:
http://gis.stackexchange.com/questions/44733/how-to-get-list-of-selected-features-in-arcgis-for-desk...

Hope that helps clarify a little... if not, then I'm off-track and let me know.

Wayne


EDIT:
I suppose to support my reasoning, and this is efficient enough for say a dozen features or so that you want to code lot numbers for at a time, then comparing 2 lists for the added selected OID, maybe something this (there may be a much simpler means, maybe difference of the 2 sets?):
>>> # state at one point of the selection
>>> listOfOIDs1 = [1,2,3,4]

>>> # state after adding to the selection (5 added)
>>> listOfOIDs2 = [1,2,3,4,5]
>>> theNewOID = [oid for oid in listOfOIDs2 if oid not in listOfOIDs1]

>>> print theNewOID
[5]
>>> 
>>> # EDIT 2:18 PM
>>> # ...or this, to compare lists:
>>> set(listOfOIDs2).difference(listOfOIDs1).pop()
5
0 Kudos
Zeke
by
Regular Contributor III
Thanks Wayne, you're correct. Listen for selection change, determine if the item is selected or deselected, and add or drop it from the list respectively. It's trivial to number at that point, but getting to that point is the problem.

There's a tool in the parcel fabric and/or local government model that does something similar when drawing a line through the lots, but we're not there yet. I also need to allow the user to set the starting number. Python add-in extensions have an event onEditorSelectionChanged(self), but I can't find any documentation on how to use it properly.What I really need is a property like isSelected I could check.

One option might be to forget selection and create two buttons, a start and a stop, with each click after start incrementing by one until the user clicks stop. Not sure if I can hijack the cursor to do that though. I'll take a look at your method. Thanks again.
0 Kudos
T__WayneWhitley
Frequent Contributor
Actually, I think it could be simpler than that - although I haven't had time to test this yet...

I think the key may be to declare a global variable.  Here's an example of passing a global layer var, see this posted by Jake Skinner:

http://forums.arcgis.com/threads/99718-How-to-populate-Add-In-Combo-Box-with-attributes#2


Pay no mind to the 'onSelChange' function which only has to do with listening to the change in the combobox selected item...
what I'm referring to is the ability to pass variables between functions - the onFocus function establishes the 'layer' obj which then can be referenced by SelectLayerByAttribute operation in the onSelChange function.

In a similar way I don't see why not you can't initiate a global list variable that you can use to keep track of a the currently selected features and use to compare the 'new selection' via the onEditorSelectionChanged event.  Like I said, haven't tested it, but the general concept is that onEditorSelectionChanged should be fired and you should be able to track the order of selected features in a global list.

Hope that helps.

Wayne
0 Kudos
Zeke
by
Regular Contributor III
Thanks. If I understand correctly (always questionable), I think that's the direction I'm going. Not a finished product below, but where I'm heading...

import arcpy
import pythonaddins

lstChanged = []
fc = r"C:\path\to\layer\Lots"
field = "OBJECTID"

class CheckSelectionChange(object):
    """Implementation for PyAddInWizard_addin.ext1 (Extension)"""
    def __init__(self):
        # For performance considerations, please remove all unused methods in this class.
        self.enabled = True

    def onEditorSelectionChanged(self):
        fc = r"C:\Users\gkeith\Documents\Projects\Laurie\LaurieTest.gdb\Lots"
        field = "OBJECTID"
        with arcpy.SearchCursor(fc, field) as row:
            if row.field in lstChanged:
                lstChanged.pop(row.field)    # this may be unnecessary, but will try it
            else:
                lstChanged.append(row.field)

class CreateLotNum(object):
    """Implementation for PyAddInWizard_addin.btn1 (Button)"""
    def __init__(self):
        self.enabled = True
        self.checked = False

    def onClick(self): # button event handler
        lstDoubleCheck = []
        with arcpy.SearchCursor(fc, field) as row:
            lstDoubleCheck.append(row.field)
    # plan to check lstDoubleCheck, all currently selected parcels, against lstChanged, any that were listed in onEditor...
0 Kudos
Zeke
by
Regular Contributor III
Ok, getting there slowly, not there yet. In the code below, as soon as I start editing, onEditorSelectionChanged runs through all the OIDs, although none are selected at that point - no change has occurred. When I then select one, it runs through them again, but raises an 'index out of range' exception on .pop(), presumably for the one I selected. I thought SearchCursor honored selections. Then, when I click the button, onClick raises a 'list object not callable' exception on 'if lstChanged(item) not in lstDoubleCheck'. This is puzzling. I must be missing something simple, but nothing's popping out at me.

import arcpy
import pythonaddins

lstChanged = []
fc = r"C:\path\to\file\Lots"
field = "OBJECTID"

class CheckSelectionChange(object):
    """Implementation for PyAddInWizard_addin.ext1 (Extension)"""
    def __init__(self):
        # For performance considerations, please remove all unused methods in this class.
        self.enabled = True

    def onEditorSelectionChanged(self):
        for row in arcpy.SearchCursor(fc, '', '', field):
            print "Row: " + str(row.OBJECTID) + "\n"    # just for testing
            if row.OBJECTID in lstChanged:
                lstChanged.pop(row.OBJECTID)    # raises error
            else:
                lstChanged.append(row.OBJECTID)

class CreateLotNum(object):
    """Implementation for PyAddInWizard_addin.btn1 (Button)"""
    def __init__(self):
        self.enabled = True
        self.checked = False

    def onClick(self):
        lstDoubleCheck = []
        for row in arcpy.SearchCursor(fc, '', '', field):
            lstDoubleCheck.append(row.OBJECTID)

        for item in lstChanged:
            if lstChanged(item) not in lstDoubleCheck:    # raises error
                lstChanged.append(item)
        with open(r"C:\Temp\id.txt", "w") as f:    # also for testing
            for item in lstChanged:
                f.write(lstChanged(item) + "\n")
0 Kudos
T__WayneWhitley
Frequent Contributor
I have little to add, just was checking back on your progress...

Be glad that line did error out because that's potentially an infinite loop - if you didn't already catch this (probably you have), I think you meant:
for item in lstChanged:
            if item not in lstDoubleCheck:
                lstDoubleCheck.append(item)  # not lstChanged



I'm interested to see how this develops if you're still working on it, so I hope to get more time today to check back in...

Thanks,
Wayne
0 Kudos
Zeke
by
Regular Contributor III
Thanks Wayne. It's kind of on the back burner for now. Turns out a former co-worker wrote a vb.net tool that does this (or similar, not sure since the tool is used by someone else) a few years ago. The problem with that is that the installer he included throws a LoadException error and won't install on Windows 7 (or maybe the problem is with Arc 10.1 and above). It appears to be an issue of the dll looking for a missing or bad pathed assembly.  It was installed previously on an XP machine with Arc 10.0, which was upgraded to 10.2.2, and appears there. Which would be good except that machine doesn't support the SQL Native Client drivers needed to edit. So now we're trying to locate an old laptop that has Visual Studio so we can open and hopefully fix it. For reasons above my pay grade VS Express is not an option.

In regards to your fix to the code, though, I'm confused. Why would I append to lstDoubleCheck? That will have the data sorted in OID order. I'm only using it to make sure that any selected features weren't missed in lstChanged. If I do work on this further, I may see if I can skip the batch update of all features, and just increment a counter and update individually as a feature is selected. I think that may be the vb tool behavior.
0 Kudos
T__WayneWhitley
Frequent Contributor
Sorry for the misunderstanding - I really didn't mean that as part of the overall solution, only to point out the immediate source of the error message.  More explicitly, this:
>>> aList = list()
>>> aList.append(1)
>>> 
>>> # not allowed:
>>> aList(0)

Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    aList(0)
TypeError: 'list' object is not callable
>>> 
>>> # allowed:
>>> aList[0]
1
>>> 
>>> # or:
>>> for item in aList:
 print item

 
1
>>> 


My 2nd point about the infinite loop was just that even if you didn't get that error, your loop that would have been entered would be a process you'd have to kill (unless the lists were identical), because the item is being added to your list so that the 'not in' logic would never evaluate false...if that now makes sense?

Sorry for the confusion...if I have anything else to contribute later, it'll be more toward the overall solution, lol!

Wayne

Edit:
This is important to see, about infinite loop situation:
>>> lstChanged = [1,2,3]
>>> lstDoubleCheck = [1,2]
>>> 
>>> for item in lstChanged:
            if item not in lstDoubleCheck:     # error fixed here
                lstChanged.append(item)  # new error introduced here
                if len(lstChanged) == 20: # for demo purposes, forced a break
   break

  
>>> # so here's what we have:
>>> print lstChanged
[1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
>>> # the integer item '3' would have been appended infinitely.
0 Kudos
T__WayneWhitley
Frequent Contributor
Interesting problem, and good practice for programming add-ins, I think...
I said I'd report back if I had anything more in the way of a working solution.  At this point, if you consider this a problem with more than 1 component [I agree with you; it's (1) get the order of selection of features and (2) code a designated field in that selection order with a user-specified range of values (a starting number and step interval, essentially)].

So far, I solved the 1st component, the assembling of the list of OIDs.  Only test code, so I'm sure it could be done several different ways.  For simplicity, this is a start - which I thought was important to more clearly state what I meant about global scope of a variable, managing the list values, and debugging...and I left many print messages in as they aided me in debugging:
import arcpy
import pythonaddins

orderBySelOIDs = list()
orderBySelOIDs.append('is it alive?')

class globalListClass(object):
    def __init__(self):
        self.enabled = True
        print 'self aware?'
    def onEditorSelectionChanged(self):
        print 'onEditorSelectionChanged fired...'
        FIDreturn = arcpy.Describe(someLayer).FIDSet.split(';')
        if FIDreturn != ['']:
            selOIDs = [int(OID) for OID in FIDreturn]
            print 'currently selected oids: {0}'.format(str(selOIDs))
            uniqueSel = set(selOIDs).difference(orderBySelOIDs)
            while len(uniqueSel) != 0:
                orderBySelOIDs.append(uniqueSel.pop())
                print 'oid list in order selected: {0}'.format(str(orderBySelOIDs))
        else:
            print 'no features in selection (or selection has been cleared)'
            print 'this executes also on starting Editor...'
            print 'what does orderBySelOIDs contain?: {0}'.format(str(orderBySelOIDs))
            del orderBySelOIDs[:]
            print 'the ordered list has been reset (emptied):  {0}'.format(str(orderBySelOIDs))
    def openDocument(self):
        getLayers = arcpy.mapping.ListLayers(arcpy.mapping.MapDocument('current'))
        if len(getLayers) != 0:
            global someLayer
            someLayer = getLayers[0]
            print 'openDocument fired, setting global layer: {0}'.format(someLayer.name)


This, for example, is some of my feedback (in ArcMap's Python window):
>>> 
openDocument fired, setting global layer: clip500
>>> 
onEditorSelectionChanged fired...
no features in selection (or selection has been cleared)
this executes also on starting Editor...
what does orderBySelOIDs contain?: ['is it alive?']
the ordered list has been reset (emptied):  []
>>> 
onEditorSelectionChanged fired...
currently selected oids: [3]
oid list in order selected: [3]
>>> 
onEditorSelectionChanged fired...
currently selected oids: [1]
oid list in order selected: [3, 1]
>>> 
onEditorSelectionChanged fired...
currently selected oids: [4]
oid list in order selected: [3, 1, 4]
>>> 
onEditorSelectionChanged fired...
currently selected oids: [2]
oid list in order selected: [3, 1, 4, 2]
>>> 
onEditorSelectionChanged fired...
no features in selection (or selection has been cleared)
this executes also on starting Editor...
what does orderBySelOIDs contain?: [3, 1, 4, 2]
the ordered list has been reset (emptied):  []


I suppose a small bit of explanation is in order - just to include a few points:
-- Again, for simplicity, I included only 3 functions - __init__(self), onEditorSelectionChanged(self), and openDocument(self).
-- As part of initializing, getting a map layer was necessary - this was done with the openDocument(self) function, fetching the 1st layer in the map.  This is where a global layer variable was set up (someLayer) so that it could be used 'outside' the scope of the immediate function (where it was created) -- it was created inside this function because I didn't want to re-create the layer reference every time onEditorSelectionChanged fires...I suppose onStartEditing(self) would have been a better choice.
-- onEditorSelectionChanged is fired even just starting the edit session, so I chose to use the list to send myself a message which I'd change on occasion to make sure I'm running the currently installed code (the current version I assume I'm working on).  This serves no useful purpose outside of debugging and I just left it in.
-- FIDSet was used on Describe of the global layer (someLayer) instead of a cursor.  This returns a semi-colon ( ; ) separated string of the selected feature OIDs.
-- orderBySelOIDs holds the order of features selected by OID...upon clear selection (or no features targeted on a selection), the list is cleared -- not redefined since that introduces another scope problem, but emptied.  Strangely, there's no 'clear' method on list objects (there is for 3.x I'm told), so I rather clumsily used the following (but it works and the list should be short, so not a great concern for efficiency):
while len(orderBySelOIDs) != 0:  orderBySelOIDs.pop()
EDIT:  Substituted the following line instead--
del orderBySelOIDs[:]
-- This test script accepts either use of the Edit tool on the editing toolbar or the Select Features tool, just as you'd expect in the Editing environment --- also, handling is added for selection of multiple features at a time, but simply loads the list in the OID order, appending whatever remaining OIDs are not already in the list.


So that's fairly basic handling of constructing a list of feature OIDs in the order of selected features during an edit session...the 2nd component of actually using the list and handling the field coding of sequential numbers, etc., remains.

If I have time for that later, I'll take another stab at it...back-burner as you said for now, but glad to at least clarify part of this thread.  My guess it's just a matter of reading the list from memory, to then pass to the logic of another add-in to accept user input and probably running an update cursor --- read perhaps a dictionary to look up position in the sequence and assign the field value....

Wayne
0 Kudos