'''
Name: avi_audit.py

Overview: Provides methods to audit AVI geodatabase submission.

Description:
    Provides a set of methods to aid in the audit of AVI data contained
    in the File Geodatabase format.
    
Notes:

Author: Doug Crane
        Sept, 2009

Modifications:
    Sept 2012 Upgraded to 10.1

'''

import pdb
import os
import sys
import datetime

import arcpy

# General SRD utilities that may be needed.
from srd.srd_exception import *
import srd.srd_logging as srd_logging
import srd.srd_misc as srd_misc
import srd.srd_temp_ws as srd_temp_ws
import srd.srd_recordset as srd_recordset

from avi_record import *
from avi_schema import *
            
__author__ = 'Doug Crane'
__version__ = '1.0'

__all__ = ['AVI_Audit']

# ----------------------------------------------------------
class AVI_Audit(object):
    '''
    Provides methods for performing spatial audit of
    AVI geodatabase feature class
    '''
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, gdbPath):
        '''
        Parameters:
            gdbPath - path to the FGDB that will contain the
                      audit feature classes.
                      
        '''

        self.fgdbPath = gdbPath
        self.fgdbDir = os.path.dirname(gdbPath)
        self.fgdbName = os.path.basename(gdbPath)

        name,ext = os.path.splitext(self.fgdbName)
        logFileName = '%s.log' % name
        self.logFilePath = os.path.join(self.fgdbDir, logFileName)
        self._logChn = srd_logging.SRD_Log(self.logFilePath)
        
        # Create the audit FGDB if it does not exist.
        if not arcpy.Exists(self.fgdbPath):
            arcpy.CreateFileGDB_management(self.fgdbDir, self.fgdbName)
                                    
        # Name of feature class that will indicate overlapping polygons.
        self.overlapFCName = 'overlap'
        self.overlapFCPath = os.path.join(gdbPath, self.overlapFCName)

        # Name of feature class that will indicate gaps between polygons.
        self.gapsFCName = 'gaps'
        self.gapsFCPath = os.path.join(gdbPath, self.gapsFCName)

        self.boundaryFCName = 'boundary'
        self.boundaryFCPath = os.path.join(gdbPath, self.boundaryFCName)

        # Name of feature class that will indicate neighbor polygons with matching attributes.
        self.attNeightborFCName = 'att_neighbor'
        self.attNeightborFCPath = os.path.join(gdbPath, self.attNeightborFCName)

        self.minAreaFCName = 'min_area'
        self.minAreaFCPath = os.path.join(gdbPath, self.minAreaFCName)

        self.idFieldName = ''
        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def version(self):
        '''
        Current version of software.
        '''
        return 'Sept 19, 2012'
            
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def setLogFile(self, logPath):
        '''
        Sets the path to a log file that will contain audit messages.
        '''
        self.logFilePath = logPath
        self._logChn = srd_logging.SRD_Log(logPath)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _initLogFile(self):
        '''
        Logs header information to the log file
        '''

        self._logChn.logMsg('\n\n***\n AVI Spatial Audit Report\n***\n')
        self._logChn.logMsg('Audit Version: %s' % self.version())
        self._logChn.logMsg('AVI Feature Class: %s' % self.aviFCPath)
        self._logChn.logMsg('Date: %s' % datetime.date.today())
        self._logChn.logMsg('Audit Run By: %s' % os.environ.get('USERNAME'))
        self._logChn.logMsg('\n------------------------------------------------\n')
        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def auditAll(self, aviFCPath,
                 repairGeometry = True,
                 chkFields = True,
                 chkOverlap = True,
                 chkGaps = True,
                 chkNeighbors = True,
                 chkMultiParts = True,
                 chkDupPolynums = True,
                 chkMinStandSize = False):
        '''
        Performs all audit checks on AVI feature class.

        Parameters:
            aviFCPath - path to AVI feature class to audit.
            
        '''

        # Some validation first.
        if not arcpy.Exists(aviFCPath):
            msg = 'AVI Feature Class: %s not found' % aviFCPath
            raise SRD_Exception(msg)

        featureCnt = int(arcpy.GetCount_management(aviFCPath).getOutput(0))
        if featureCnt < 1:
            msg = 'AVI Feature Class:\n %s\n has no polygons to process, cannot audit' % aviFCPath
            raise SRD_Exception(msg)
        else:
            self._logChn.logMsg('\n***\n*** %s polygons to audit' % featureCnt)
        
        self.aviFCPath = aviFCPath
        self.aviFCName = os.path.basename(aviFCPath)

        self._initLogFile()

        if chkFields:
            self.checkForMissingFields(aviFCPath)        
            self.checkForExtraFields(aviFCPath)
            self.checkFieldStructure(aviFCPath)

        if repairGeometry:
            self._logChn.logMsg('\n---------------- Running Repair Geometry...')
            arcpy.RepairGeometry_management(aviFCPath, 'DELETE_NULL')

        self.buildOuterBoundary(aviFCPath)
        self._logChn.logMsg('\n---------------- Generating outer boundary...')

        if chkOverlap:
            self.checkForOverlap(aviFCPath)

        if chkGaps:
            self.checkForGaps(aviFCPath)

        if chkNeighbors:
            self.checkNeighboringAttributes(aviFCPath)

        if chkMultiParts:
            self.checkForMultiParts(aviFCPath)

        if chkDupPolynums:
            self.checkForDuplicatePolyNumbers(aviFCPath)

##        if chkMinStandSize:
##            self.checkMinStandSize(aviFCPath)
        
        self._logChn.logMsg('\n--------------------------------------------')
        self._logChn.logMsg('\n***\n*** Spatial audit complete')
        self._logChn.logMsg('Audit results written to %s' % self.logFilePath)
        self._logChn.logMsg('Error features written to %s\n' % self.fgdbPath)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def checkForDuplicatePolyNumbers(self, aviFCPath):
        '''
        Makes sure that all POLY_NUM values are unique.
        '''

        self._logChn.logMsg('\n---------------- Checking for duplicate POLY_NUM values')
        
        fieldList = ['poly_num',]

        rsObj = srd_recordset.SRD_Recordset(aviFCPath)
        
        selCnt = int(arcpy.GetCount_management(aviFCPath).getOutput(0))
        arcpy.SetProgressor('step', 'Checking for duplicate POLY_NUMs ...', 0, selCnt, 1)

        errCnt = 0
        polyNumDict = {}
        with arcpy.da.SearchCursor(aviFCPath, fieldList) as cursor:
            for row in cursor:

                # Get a dictionary or values to enable easy access.
                recDict = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)

                polyNum = recDict['poly_num']

                if polyNum in polyNumDict:
                    errCnt += 1
                    self._logChn.logMsg(' *** POLY_NUM: %s already exists, must be unique' % polyNum)
                else:
                    polyNumDict[polyNum] = polyNum
                
                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()
                
        if errCnt > 0:
            self._logChn.logMsg('\n*** ERROR %s duplicate POLY_NUMs detected\n***' % errCnt)
        else:
            self._logChn.logMsg('\n   No duplicate POLY_NUM errors detected\n')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def checkForMissingFields(self, aviFCPath):
        '''
        Makes sure that all the required AVI fields are present.
        '''

        self._logChn.logMsg('\n---------------- Checking for missing fields')

        # Obtain set of AVI fields that should be present.
        aviRec = AVI_Record(aviFCPath)
        missingFieldList = []

        # Add to standard set of AVI fields some addiional items required for contract.
        requiredFieldList = aviRec.allFieldNames[:]

        fcFieldNameList = [field.name.lower() for field in arcpy.ListFields(aviFCPath)]
        for fieldName in requiredFieldList:
            if not fieldName.lower() in fcFieldNameList:
                missingFieldList.append(fieldName)

        errCnt = len(missingFieldList)
        if errCnt > 0:
            self._logChn.logMsg('\n*** ERROR %s missing fields detected\n***' % errCnt)
            self._logChn.logMsg('\n *** Following AVI fields are missing:')
            for fieldName in missingFieldList:
                self._logChn.logMsg('   %s' % fieldName)
        else:
            self._logChn.logMsg('\n   No missing AVI fields\n')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def checkFieldStructure(self, aviFCPath):
        '''
        Validates the structure of the AVI fields.
        '''

        self._logChn.logMsg('\n---------------- Checking for incorrect field structure')

        aviSchemaObj = AVI_Schema()
        
        errList = aviSchemaObj.validateFieldStructure(aviFCPath)
        if errList:
            self._logChn.logMsg('\n*** ERROR %s invalid field structure errors detected\n***' % len(errList))
            self._logChn.logMsg('\n *** Following AVI fields have incorrect structure:')
            for errMsg in errList:
                self._logChn.logMsg(errMsg)
        else:
            self._logChn.logMsg('\n   No field structure errors detected\n')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def checkForExtraFields(self, aviFCPath):
        '''
        Flag any non-AVI fields.
        '''

        self._logChn.logMsg('\n---------------- Checking for non-required fields')

        # Add to standard set of AVI fields some addiional items
        # required for supplemental information to be compiled.
        aviRec = AVI_Record(aviFCPath)
        requiredFieldList = aviRec.allFieldNames[:]

        # Get list of existing fields in the AVI feature class.
        fieldNames = [field.name.lower() for field in arcpy.ListFields(aviFCPath)]

        # Ignore the following fields.
        if 'objectid' in fieldNames:
            fieldNames.remove('objectid')
        if 'shape_length' in fieldNames:
            fieldNames.remove('shape_length')
        if 'shape_area' in fieldNames:
            fieldNames.remove('shape_area')
        if 'shape' in fieldNames:
            fieldNames.remove('shape')
        
        extraFieldList = []

        for fieldName in fieldNames:
            if not fieldName in requiredFieldList:
                extraFieldList.append(fieldName)

        errCnt = len(extraFieldList)
        if errCnt > 0:
            self._logChn.logMsg('\n*** ERROR %s extra fields detected\n***' % errCnt)
            self._logChn.logMsg('\n *** Following are not AVI fields:')
            for fieldName in extraFieldList:
                self._logChn.logMsg('   %s' % fieldName)
        else:
            self._logChn.logMsg('\n   No non-AVI fields detected\n')
        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def checkForMultiParts(self, aviFCPath):
        '''

        Makes sure there are no multi-part polygons.
        
        Parameters:
            aviFCPath - path to AVI feature class to audit.
            
        '''

        self._logChn.logMsg('\n---------------- Checking for multi-part polygons')

        selCnt = int(arcpy.GetCount_management(aviFCPath).getOutput(0))
        arcpy.SetProgressor('step', 'Checking for multi-part polygons ...', 0, selCnt, 1)

        fieldList = ['OID@', 'SHAPE@']
        errCnt = 0
        with arcpy.da.SearchCursor(aviFCPath, fieldList) as cursor:
            for row in cursor:

                oid = row[0]
                feat = row[1]

                if feat.partCount > 1:
                    errCnt += 1
                    self._logChn.logMsg('   %s parts detected for OBJECTID: %s' %(feat.partCount, oid))
                
                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        if errCnt > 0:
            self._logChn.logMsg('\n*** ERROR %s multi-part features detected\n***' % errCnt)
        else:
            self._logChn.logMsg('\n   No multi-part errors detected\n')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def checkForOverlap(self, aviFCPath):
        '''

        Checks for any overlap of polygons. Will create a feature
        class indicating the areas of overlap.
        
        Parameters:
            aviFCPath - path to AVI feature class to audit.
            
        '''

        self._logChn.logMsg('\n---------------- Checking for overlap')

        if arcpy.Exists(self.overlapFCPath):
            arcpy.Delete_management(self.overlapFCPath)

        fcArg = '"%s"' % aviFCPath
        arcpy.Intersect_analysis(fcArg, self.overlapFCPath, 'ALL', .001)

        featureCnt = int(arcpy.GetCount_management(self.overlapFCPath).getOutput(0))
        if featureCnt > 0:
            self._logChn.logMsg('\n*** ERROR %s overlapping features detected\n***' % int(featureCnt/2))
            self._logChn.logMsg('*** Error features written to: \n%s ' % self.overlapFCPath)
        else:
            # If no features then delete the feature class.
            arcpy.Delete_management(self.overlapFCPath)
            self._logChn.logMsg('\n   No overlap errors detected\n')
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildOuterBoundary(self, srcPolyFC):
        '''
        Builds the outer boundary of a polygon feature class by
        performing a general dissolve on the feature class.

        Parameters:
            srcPolyFC - polygon feature class to generate boundary for.

        '''

        # Does not always perform a complete dissolve on first pass
        # so perform the dissolve twice.
        self._logChn.logMsg('Performing initial dissolve to create data boundary..')

        tmpFGDB = srd_temp_ws.SRD_TempWS('tmp_ws')
        
        tmpIntFC = os.path.join(tmpFGDB.FGDBPath, 'tmp_int')
        arcpy.Dissolve_management(srcPolyFC, tmpIntFC, '', '', 'SINGLE_PART')

        self._logChn.logMsg('Performing secondary dissolve..')
        arcpy.Dissolve_management(tmpIntFC, self.boundaryFCPath, '', '', 'SINGLE_PART')

        tmpFGDB.deleteAllTempWS('tmp_ws')
        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def checkForGaps(self, aviFCPath):
        '''

        Checks for any gaps between polygons. Will create a feature
        class containing polygons where potential gaps are located.
        
        Parameters:
            aviFCPath - path to AVI feature class to audit.
            
        '''

        self._logChn.logMsg('\n---------------- Checking for potential gaps')

        if arcpy.Exists(self.gapsFCPath):
            arcpy.Delete_management(self.gapsFCPath)

        tmpFGDB = srd_temp_ws.SRD_TempWS('tmp_ws')
        
        tmpFCName = 'tmp_gap'
        tmpFCPath = os.path.join(tmpFGDB.FGDBPath, tmpFCName)

        # Use self union with NO_GAPS to identify potential gaps.
        unionList = '"%s" #' % aviFCPath
        arcpy.Union_analysis(unionList, tmpFCPath, 'ALL', .001, 'NO_GAPS')

        tmpFL = '%s_fl' % tmpFCName
        arcpy.MakeFeatureLayer_management(tmpFCPath, tmpFL)
        sql = '"FID_%s" < 0' % os.path.basename(aviFCPath)
        arcpy.SelectLayerByAttribute_management(tmpFL, 'NEW_SELECTION', sql)

        featureCnt = int(arcpy.GetCount_management(tmpFL).getOutput(0))
        if featureCnt > 0:
            arcpy.CopyFeatures_management(tmpFL, self.gapsFCPath)
            self._logChn.logMsg('\n*** ERROR %s potential gaps detected\n***' % featureCnt)
            self._logChn.logMsg('*** Gap features written to: \n%s ' % self.gapsFCPath)
        else:
            self._logChn.logMsg('\n   No gap errors detected\n')

        tmpFGDB.deleteAllTempWS('tmp_ws')
        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildAttributeDictionary(self, aviFCPath):
        '''
        Builds and returns a dictionary or attributes for
        each polygon. This will be used to check neighboring attributes.

        Parameters:
            aviFCPath - path to AVI feature class to audit.

        Returns:
            tuple containing attribute dictionary and area dictionary.
        '''

        aviObj = AVI_Record(aviFCPath)

        fieldList = aviObj.allFieldNames[:]

        # Remove fields that are not needed for neighbor check.
        fieldList.remove('poly_num')
        fieldList.remove('initials')
        fieldList.remove('uinitials')
        fieldList.remove('photo_yr')

        # Dictionary will be keted by OID
        oidFieldName = arcpy.Describe(aviFCPath).OIDFieldName
        oidFieldName = oidFieldName.lower()
        fieldList.append(oidFieldName)

        # Make sure all the fields exist.
        missingFieldList = []
        fcFieldNameList = [field.name.lower() for field in arcpy.ListFields(aviFCPath)]
        for fieldName in fieldList:
            if not fieldName.lower() in fcFieldNameList:
                missingFieldList.append(fieldName)

        if missingFieldList:
            raise SRD_Exception('AVI Feature Class is missing fields: %s' % missingFieldList)
        
        rsObj = srd_recordset.SRD_Recordset(aviFCPath)

        # Build a dictionary of our attributes for each polygon so
        # we can detect matching attributes across borders.

        selCnt = int(arcpy.GetCount_management(aviFCPath).getOutput(0))
        arcpy.SetProgressor('step', 'Building Attribute Key...', 0, selCnt, 1)

        attDict = {}
        with arcpy.da.SearchCursor(aviFCPath, fieldList) as cursor:
            for row in cursor:

                recDict = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)
            
                oid = recDict[oidFieldName]
                # Set OID value to 0 in dictionary since it is always unique
                # and we will be comparing attributes to see if same value.
                recDict[oidFieldName] = 0
                
                attDict[oid] = recDict.values()
                
                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        return attDict
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildOIDNeighboringDict(self, aviFCPath):
        '''
        Builds a dictionary that contains all the neighboring OIDs
        for a polygon.

        Parameters:
            aviFCPath - path to AVI feature class to audit.

        Returns:
            Dictionary with the OID as the key and the list of
            neighboring polygons as the value associated with the key.
        '''

        tmpFGDB = srd_temp_ws.SRD_TempWS('tmp_ws')
        
        tmpTblName = 'tmp_oid_nbrs'
        tmpTblPath = os.path.join(tmpFGDB.FGDBPath, tmpTblName)

        # Use the OID to identify polygons.
        oidFieldName = arcpy.Describe(aviFCPath).OIDFieldName

        # First step is to generate neighboring OID table.
        # Note that only one side option used to eliminate duplicates.
        arcpy.PolygonNeighbors_analysis(aviFCPath, tmpTblPath, oidFieldName, 'NO_AREA_OVERLAP', 'NO_BOTH_SIDES', .001)

        selCnt = int(arcpy.GetCount_management(aviFCPath).getOutput(0))
        arcpy.SetProgressor('step', 'Building OID neighbor list ...', 0, selCnt, 1)

        leftFieldName = 'src_%s' % oidFieldName
        rightFieldName = 'nbr_%s' % oidFieldName
        fieldList = [leftFieldName, rightFieldName]

        oidDict = {}
        with arcpy.da.SearchCursor(tmpTblPath, fieldList) as cursor:
            for row in cursor:

                leftFID = row[0]
                rightFID = row[1]
                
                if leftFID in oidDict:
                    oidList = oidDict[leftFID]
                else:
                    oidList = []

                if not rightFID in oidList:
                    oidList.append(rightFID)

                oidDict[leftFID] = oidList

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        return oidDict        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def checkNeighboringAttributes(self, aviFCPath):
        '''

        Checks for any gaps between polygons. Will create a feature
        class containing polygons where potential gaps are located.

        The feature class is converted to lines that indicate the
        left/right polygons for each line. The list of attributes
        to be evaluated is read from the polygon feature class
        and based on the left/right IDs, a comparison is performed.
        If the attributes are the same then the polygon is flagged
        using the _ATT_NEIGHBOR_ERR field that is added to the polygon
        feature class.
        
        Parameters:
            aviFCPath - path to AVI feature class to audit.
            
        '''

        self._logChn.logMsg('\n---------------- Checking neighboring attributes')

        flagFieldName = '_ATT_NEIGHBOR_ERR'
        if not arcpy.ListFields(aviFCPath, flagFieldName):
            self._logChn.logMsg('Adding field: %s to flag polygons with same attributes as neighbor' % flagFieldName)
            arcpy.AddField_management(aviFCPath, flagFieldName, 'SHORT')

        self._logChn.logMsg('Calculating %s' % flagFieldName)
        arcpy.CalculateField_management(aviFCPath, flagFieldName, 0, 'PYTHON')

        # Build set of neighbors dictioanry.
        oidNbrDict = self.buildOIDNeighboringDict(aviFCPath)
        
        attDict = self.buildAttributeDictionary(aviFCPath)

        selCnt = int(arcpy.GetCount_management(aviFCPath).getOutput(0))
        arcpy.SetProgressor('step', 'Checking neighbor attributes ...', 0, len(oidNbrDict), 1)

        # Use the Left/Right OID dictionary to compare attributes.
        attErrorDict = {}
        for leftFID,nbrList in oidNbrDict.items():
            
            leftAtt = attDict[leftFID]

            for rightFID in nbrList:
                rightAtt = attDict[rightFID]

                # Do our attributes match.
                if leftAtt == rightAtt:
                    # Make sure we do not already have an entry for this error.
                    leftRightKey = str(leftFID) + '_' + str(rightFID)
                    if not leftRightKey in attErrorDict:
                        rightLeftKey = str(rightFID) + '_' + str(leftFID)
                        if not rightLeftKey in attErrorDict:
                            attErrorDict[leftRightKey] = (leftFID, rightFID)

            arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        # Flag any polygons with attribute errors.

        # First build list of all polygons that have been
        # flagged with a neighboring attribute error.
        oidErrorList = []
        for val in attErrorDict.values():
            oid1 = val[0]
            if not oid1 in oidErrorList:
                oidErrorList.append(oid1)
            oid2 = val[1]
            if not oid2 in oidErrorList:
                oidErrorList.append(oid2)

        if oidErrorList:
            fieldList = ['OID@', flagFieldName]
            selCnt = int(arcpy.GetCount_management(aviFCPath).getOutput(0))
            arcpy.SetProgressor('step', 'Setting error flag...', 0, selCnt, 1)

            with arcpy.da.UpdateCursor(aviFCPath, fieldList) as cursor:
                for row in cursor:

                    oid = row[0]
                    if oid in oidErrorList:
                        flagVal = 1
                    else:
                        flagVal = 0
                    row[1] = flagVal
                    
                    cursor.updateRow(row)
                    
                    arcpy.SetProgressorPosition()
                    
            arcpy.ResetProgressor()

            # Log the OIDs of any offending polygons.
            self._logChn.logMsg('\n***\n*** The OBJECTIDs of following polygons have matching attributes across their neighboring boundaries:\n')
            for val in attErrorDict.values():
                self._logChn.logMsg('%s | %s ' % (val[0], val[1]))            

            # Copy the features so can be reviewed more easily.
            tmpFL = 'tmp_fl'
            sql = '"%s" > 0' % flagFieldName
            arcpy.MakeFeatureLayer_management(aviFCPath, tmpFL, sql)
                    
            featureCnt = int(arcpy.GetCount_management(tmpFL).getOutput(0))
            self._logChn.logMsg('\n*** ERROR %s neighboring attribute errors detected\n***' % featureCnt)
            self._logChn.logMsg('*** Error features written to: \n%s ' % self.attNeightborFCPath)
            arcpy.CopyFeatures_management(tmpFL, self.attNeightborFCPath)

        if not oidErrorList:
            self._logChn.logMsg('   No neighboring attribute errors detected')

        if arcpy.ListFields(aviFCPath, flagFieldName):
            arcpy.DeleteField_management(aviFCPath, flagFieldName)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def checkMinStandSize(self, aviFCPath):
        '''

        
        Parameters:
            aviFCPath - path to AVI feature class to audit.
            
        '''

        self._logChn.logMsg('\n---------------- Checking area less than 1/10 ha as possible errors')

        flagFieldName = '_MIN_AREA_ERR'
        if not arcpy.ListFields(aviFCPath, flagFieldName):
            self._logChn.logMsg('Adding field: %s to flag polygons with minimum stand size error' % flagFieldName)
            arcpy.AddField_management(aviFCPath, flagFieldName, 'SHORT')


        selCnt = int(arcpy.GetCount_management(aviFCPath).getOutput(0))
        arcpy.SetProgressor('step', 'Checking stand size...', 0, selCnt, 1)

        fieldList = ['SHAPE@AREA', flagFieldName]
        errCnt = 0
        with arcpy.da.UpdateCursor(aviFCPath, fieldList) as cursor:
            for row in cursor:

                polyArea = row[0]
                if polyArea < 1000:
                    errCnt += 1
                    errFlag = 1
                else:
                    errFlag = 0

                row[1] = errFlag
                cursor.updateRow(row)
                
                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        if errCnt > 0:
            # Copy the features so can be reviewed more easily.
            tmpFL = 'tmp_fl'
            sql = '"%s" > 0' % flagFieldName
            arcpy.MakeFeatureLayer_management(aviFCPath, tmpFL, sql)
                    
            featureCnt = int(arcpy.GetCount_management(tmpFL).getOutput(0))
            if featureCnt > 0:
                self._logChn.logMsg('\n*** ERROR %s possible stand area errors detected\n***' % featureCnt)
                self._logChn.logMsg('*** Error features written to: \n%s ' % self.minAreaFCPath)
                arcpy.CopyFeatures_management(tmpFL, self.minAreaFCPath)

            arcpy.Delete_management(tmpFL)
            del tmpFL

        else:
            self._logChn.logMsg('   No stand area errors detected')

        
        if arcpy.ListFields(aviFCPath, flagFieldName):
            arcpy.DeleteField_management(aviFCPath, flagFieldName)

# ======================================================================
if __name__ == '__main__':
    
    arcpy.gp.logHistory = False
    arcpy.env.overwriteOutput = True

    fcPathArg = r'D:\test Data\B46Strata\CaribouAVIE_B46Strata.gdb\AVIE_B46'
    gdbPathArg = os.path.dirname(fcPathArg)

    myObj = AVI_Audit(gdbPathArg)
    
    ##myObj.checkForDuplicatePolyNumbers(fcPathArg)

    ##fcPathArg = r'D:\testData\AVI\Schema_tests.gdb\bad_struct'
    ##myObj.checkForMissingFields(fcPathArg)
    
    ##myObj.checkFieldStructure(fcPathArg)

    ##myObj.checkForExtraFields(fcPathArg)

    ##myObj.checkForMultiParts(fcPathArg)

    myObj.checkForOverlap(fcPathArg)

    ##myObj.buildOuterBoundary(fcPathArg)

    ##myObj.checkForGaps(fcPathArg)

    ##oidDict = myObj.buildOIDNeighboringDict(fcPathArg)

    fcPathArg = r'D:\testData\AVI\Schema_tests.gdb\AVI'
    ##recDict = myObj.buildAttributeDictionary(fcPathArg)

    fcPathArg = r'D:\testData\AVI\Schema_tests.gdb\AVI'
    ##myObj.checkNeighboringAttributes(fcPathArg)

    fcPathArg = r'D:\testData\AVI\AVI_REC.gdb\avi_src'
    ##myObj.checkNeighboringAttributes(fcPathArg)
    ##myObj.checkMinStandSize(fcPathArg)
