4

I have started recently using some of the ArcObjects inside my Python modules. Having all the useful posts and insights shared by @matt wilkie et al, I was able to get started pretty quickly (installing the comtypes with pip and downloading the 10.2 snippet from Pierssen and changing "10.2" to "10.3" everywhere).

I am trying to iterate IFeatureCursor and get all the features inside a feature class. However, I am getting back only the latest feature (with the highest ObjectID value).

There are 6 features in the feature class hence xrange(6) to keep it simple.

from comtypes.client import GetModule, CreateObject
from snippets102 import GetStandaloneModules, InitStandalone

# First time through, need to import the “StandaloneModules”. Can comment out later.
#GetStandaloneModules()
InitStandalone()

def iterate_features():

    # Get the GDB module
    esriGeodatabase = GetModule(r"C:\Program Files (x86)\ArcGIS\Desktop10.3\com\esriGeoDatabase.olb")
    esriDataSourcesGDB = GetModule(r"C:\Program Files (x86)\ArcGIS\Desktop10.3\com\esriDataSourcesGDB.olb")

    # Create a file geodatabase pointer
    file_gdb_pointer = CreateObject(progid=esriDataSourcesGDB.FileGDBWorkspaceFactory,
                                    interface=esriGeodatabase.IWorkspaceFactory)

    file_gdb = file_gdb_pointer.OpenFromFile(r"C:\GIS\arcobjects\MyData.gdb",hWnd=0)

    #access contents inside gdb
    feature_workspace = file_gdb.QueryInterface(esriGeodatabase.IFeatureWorkspace)

    in_fc = feature_workspace.OpenFeatureClass("Warehouses")

    def enum_features(in_fc):
        """returns pointers to IFeature objects inside Feature Class"""
        cur = in_fc.Search(None,True)
        for i in xrange(6):
            feature_obj = yield cur.NextFeature()

    feats = [feat for feat in enum_features(in_fc)]
    print [f.OID for f in feats]

iterate_features()

The line print [f.OID for f in feats] returns [6, 6, 6, 6, 6, 6].

What am I doing wrong? The same logic with generator/yield (def enum_features()) works fine when iterating feature classes inside the feature dataset.

the feats_OIDs = [feat.OID for feat in enum_features(in_fc)] will give correct results, [1, 2, 3, 4, 5, 6], without me making any modifications to the code. The problem seems to be in that when I create a list of features [feat for feat in enum_features(in_fc)], they all refer to the same feature (because when I explore each of them later, each of them have the same OID).

3
  • 2
    Does it work if you interactively step through it? I'm also not sure how yielding during assignment works, you may want to separate it out (ie. feature_obj = cur.NextFeature() then yield feature_obj). – Evil Genius Dec 4 '15 at 16:29
  • @Evil Genius I agree, I think that is where the problem is. I was able to get this to work by just yielding the NextFeature. – crmackey Dec 4 '15 at 17:05
  • Thanks for pointing this out with the yield statement, I've updated my answer with more details on what's wrong with the features list. – Alex Tereshenkov Dec 4 '15 at 17:46
3

I think a better approach might be to get the count first using the FeatureCount() method of the IFeatureClass Interface. This worked for me:

import arcobjects # my copy of snippets
from comtypes.client import CreateObject
import os

pars = r'C:\TEMP\frontage_test.gdb\parcels'
fc = arcobjects.OpenFeatureClass(*os.path.split(pars))

def enum_features(fc):
    import comtypes.gen.esriGeoDatabase as esriGeoDatabase
    qf = CreateObject(progid=esriGeoDatabase.QueryFilter, interface=esriGeoDatabase.IQueryFilter) #can use NewObj here too if you have it in your snippets
    count = fc.FeatureCount(qf)
    cur = fc.Search(qf, True)
    for i in xrange(count):
        yield cur.NextFeature()


for ft in enum_features(fc):
    print ft.OID

And from my arcobjects module, this is my OpenFeatureClass() function:

def OpenFeatureClass(sFileGDB, sFCName):
    InitStandalone()
    import comtypes.gen.esriGeoDatabase as esriGeoDatabase
    import comtypes.gen.esriDataSourcesGDB as esriDataSourcesGDB
    pWSF = NewObj(esriDataSourcesGDB.FileGDBWorkspaceFactory, \
                  esriGeoDatabase.IWorkspaceFactory2)
    pWS = pWSF.OpenFromFile(sFileGDB, 0)
    pFWS = CType(pWS, esriGeoDatabase.IFeatureWorkspace)

    # determine if FC exists before attempting to open
    # http://edndoc.esri.com/arcobjects/9.2/ComponentHelp/esriGeoDatabase/IWorkspace2_NameExists.htm
    #   5 = feature class datatype
    pWS2 = CType(pWS, esriGeoDatabase.IWorkspace2)
    if pWS2.NameExists(5, sFCName):
        pFC = pFWS.OpenFeatureClass(sFCName)
    else:
        pFC = None
        print '** %s not found' % sFCName

    return pFC

EDIT

@Alex Tereshenkov raised the question of how to get this into a list of pointers to IFeature objects, and this can be done with a list comprehension. So the answer is yes.

>>> features = [ft for ft in enum_features(fc)]
>>> features[:5] # lots of features, so lets just show the first few
[<POINTER(IFeature) ptr=0x2b2d930 at 2c13440>, <POINTER(IFeature) ptr=0x2b2d930 at 2c13490>, <POINTER(IFeature) ptr=0x2b2d930 at 32f5210>, <POINTER(IFeature) ptr=0x2b2d930 at 32f5260>, <POINTER(IFeature) ptr=0x2b2d930 at 32f52b0>]
>>> 

EDIT 2:

I found the problem. We do not actually want to recycle the rows. Once I changed that to false, we can get out each IFeature into a list.

cur = fc.Search(None, False) #do not recycle this IFeature object!

So now when you do this you should get an object for each row:

features = [ft for ft in enum_features(fc)]
print [ft.OID for ft in features[:5]]

This is laid out in the help docs:

The recycling parameter controls row object allocation behavior. Recycling cursors rehydrate a single feature object on each fetch and can be used to optimize read-only access, for example, when drawing. It is illegal to maintain a reference on a feature object returned by a recycling cursor across multiple calls to NextFeature on the cursor. Features returned by a recycling cursor should not be modified. Non-recycling cursors return a separate feature object on each fetch. The features returned by a non-recycling cursor may be modified and stored with polymorphic behavior.

The Geodatabase guarantees "unique instance semantics" on non-recycling feature objects fetched during an edit session. In other words, if the feature retrieved by a search cursor has already been instantiated and is being referenced by the calling application, then a reference to the existing feature object is returned.

Non-recycling feature cursors returned from the Search method MUST be used when copying features from the cursor into an insert cursor of another class. This is because a recycling cursor reuses the same geometry and under some circumstances all of the features inserted into the insert cursor may have the same geometry. Using a non-recycling cursor ensures that each geometry is unique.

9
  • 1
    No problem! And yes, you can create a list of pointers to IFeature objects with the list comprehension as you had it set up. See my edit. – crmackey Dec 4 '15 at 18:07
  • 1
    Nevermind, that didn't work...I am having the same issue as you where I have the same OID. I'll take another look after lunch. The iterator seems to be acting weird. – crmackey Dec 4 '15 at 18:12
  • 1
    I found the problem! The problem was we were re-hydrating the row object by recycling it and using the same piece of memory for the row (IFeature) pointers. Once you disable recycling, you can get an object for each row. See my edit 2. – crmackey Dec 4 '15 at 19:32
  • 1
    Works fine for me now. Thanks a lot, mate. Now I can sleep peacefully :D – Alex Tereshenkov Dec 4 '15 at 19:41
  • 1
    Haha no problem! And I know what you mean! That was driving me crazy too; I couldn't figure out why I kept getting the same row object back. I edited my "edit 2" again to add the relevant documentation that spells it out. – crmackey Dec 4 '15 at 19:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.