'''
Name: avi_record.py

Overview: Provides basic AVI record functions.

Description:
    This provides a number of functions that aid in the processing
    of AVI records from a table or feature class. 

Notes:

Author: Doug Crane
        April, 2009

Modifications:

'''

import pdb
import os
import sys

import arcpy

from srd.srd_exception import *
import srd.srd_logging as srd_logging
import srd.srd_temp_ws as srd_temp_ws

import srd.srd_recordset as srd_recordset
import srd.srd_featureclass_tools as srd_featureclass_tools
import srd.srd_featureclass_info as srd_featureclass_info
import srd.srd_fields as srd_fields

import transfer_atts_by_pt

__author__ = 'Doug Crane'
__version__ = '1.0'

__all__ = ['AVI_Record', 'AVI_RecordUtils']

# ----------------------------------------------------------
class AVI_RecordUtils(object):
    '''
    Provides some utilities for working with AVI records. The
    methods associated with the class are static so you do not
    have to create instance of class to use.

    Are also used by aviRecord class to provide matching methods
    where you wish to use within an instance of the class.
    '''

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @staticmethod
    def speciesDataList(rec, standLayer):
        '''
        Build list containing species data.

        Many AVI functions rely on the species composition of
        a particular stand layer to perform calculations. This
        function will generate a structure that contains the species
        composition for a particular stand layer.

        List contains series of lists with first item in list the
        species and the second item its percentage.

        Example: (('Sw',6),('Aw',4),('',0),('',0),('',0))

        Parameters:
            rec - dictionary containing AVI attributes with the key for
                  the dictionary being the lowercase names of the AVI 2.1
                  fields, example 'sp1'
            standLayer - (OVERSTORY/UNDERSTORY)
                Either the species information for the overstory or understory
                is returned. Set standLayer to either OVERSTORY or UNDERSTORY
                to indicate the layer species information to return.
        '''

        if standLayer.upper() == 'OVERSTORY':
            sp1Data = (rec['sp1'], rec['sp1_per'])
            sp2Data = (rec['sp2'], rec['sp2_per'])
            sp3Data = (rec['sp3'], rec['sp3_per'])
            sp4Data = (rec['sp4'], rec['sp4_per'])
            sp5Data = (rec['sp5'], rec['sp5_per'])
        elif standLayer.upper() == 'UNDERSTORY':
            sp1Data = (rec['usp1'], rec['usp1_per'])
            sp2Data = (rec['usp2'], rec['usp2_per'])
            sp3Data = (rec['usp3'], rec['usp3_per'])
            sp4Data = (rec['usp4'], rec['usp4_per'])
            sp5Data = (rec['usp5'], rec['usp5_per'])
        else:
            raise SRD_Exception('speciesDataList: Invalid standLayer value: %s passed to speciesDataList, must be either UNDERSTORY or OVERSTORY' % standLayer)
            
        return (sp1Data, sp2Data, sp3Data, sp4Data, sp5Data)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @staticmethod
    def containsCoreData(rec):
        '''
        Determines if the AVI record contains any core data.

        An AVI record can describe various types of land cover for a
        stand such as treed vegetation or naturally non-vegetated
        land. Before an AVI record can be used it must contain data for
        at least one type of land cover. This function determines if an
        AVI record has any land cover data associated with it.
        
        Core data requires that one of the following fields  
        be populated to be considered a legitimate AVI record:
            SP1 (forested land)
            NFL (non-forested land)
            NAT_NON (naturally non-vegetated
            ANTH_VEG (anthropogenic vegetated)
            ANTH_NON (anthropogenic non-vegetated)
            
        Will return True if core data detected.

        Parameters:
            rec - dictionary containing AVI field data. The key for
                  the dictionary is the lowercase name for the AVI 2.1
                  field associated with the data, example: 'sp1'
        '''

        # At least one of following fields must be defined.
        sp1 = rec['sp1'].strip()
        nfl = rec['nfl'].strip()
        nat_non = rec['nat_non'].strip()
        anth_veg = rec['anth_veg'].strip()
        anth_non = rec['anth_non'].strip()

        # Did we find any core data.
        return sp1 or nfl or nat_non or anth_veg or anth_non
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @staticmethod
    def containsCoreUnderstoryData(rec):
        '''
        Determines if the AVI record contains any core data in understory.

        An AVI record can describe various types of land cover for a
        stand such as treed vegetation or naturally non-vegetated
        land. Before an AVI record can be used it must contain data for
        at least one type of land cover. This function determines if an
        AVI record has any land cover data associated with it.
        
        Core data requires that one of the following fields  
        be populated to be considered a legitimate AVI record:
            USP1 (forested land)
            UNFL (non-forested land)
            UNAT_NON (naturally non-vegetated
            UANTH_VEG (anthropogenic vegetated)
            UANTH_NON (anthropogenic non-vegetated)
            
        Will return True if core data detected.

        Parameters:
            rec - dictionary containing AVI field data. The key for
                  the dictionary is the lowercase name for the AVI 2.1
                  field associated with the data, example: 'sp1'
        '''

        # At least one of following fields must be defined.
        sp1 = rec['usp1'].strip()
        nfl = rec['unfl'].strip()
        nat_non = rec['unat_non'].strip()
        anth_veg = rec['uanth_veg'].strip()
        anth_non = rec['uanth_non'].strip()

        # Did we find any core data.
        return sp1 or nfl or nat_non or anth_veg or anth_non

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @staticmethod
    def blankAVIRecord():
        '''
        Returns a blank AVI 2.1 record. This a dicitonary with
        the dictionary key based on the name of the AVI 2.1 field.

        For string fields the value set is '' while numeric fields
        are assigned the value 0.

        Primarily used to perform testing.
        '''
        
        blankRec = {} 
        blankRec['poly_num'] = 0
        blankRec['moist_reg'] = ''
        blankRec['density'] = ''
        blankRec['height'] = 0
        blankRec['sp1'] = ''
        blankRec['sp1_per'] = 0
        blankRec['sp2'] = ''
        blankRec['sp2_per'] = 0
        blankRec['sp3'] = ''
        blankRec['sp3_per'] = 0
        blankRec['sp4'] = ''
        blankRec['sp4_per'] = 0
        blankRec['sp5'] = ''
        blankRec['sp5_per'] = 0
        blankRec['struc'] = ''
        blankRec['struc_val'] = 0
        blankRec['origin'] = 0
        blankRec['tpr'] = ''
        blankRec['initials'] = ''
        blankRec['nfl'] = ''
        blankRec['nfl_per'] = 0
        blankRec['nat_non'] = ''
        blankRec['anth_veg'] = ''
        blankRec['anth_non'] = ''
        blankRec['mod1'] = ''
        blankRec['mod1_ext'] = 0
        blankRec['mod1_yr'] = 0
        blankRec['mod2'] = ''
        blankRec['mod2_ext'] = 0
        blankRec['mod2_yr'] = 0
        blankRec['data'] = ''
        blankRec['data_yr'] = 0
        blankRec['umoist_reg'] = ''
        blankRec['udensity'] = ''
        blankRec['uheight'] = 0
        blankRec['usp1'] = ''
        blankRec['usp1_per'] = 0
        blankRec['usp2'] = ''
        blankRec['usp2_per'] = 0
        blankRec['usp3'] = ''
        blankRec['usp3_per'] = 0
        blankRec['usp4'] = ''
        blankRec['usp4_per'] = 0
        blankRec['usp5'] = ''
        blankRec['usp5_per'] = 0
        blankRec['ustruc'] = ''
        blankRec['ustruc_val'] = 0
        blankRec['uorigin'] = 0
        blankRec['utpr'] = ''
        blankRec['uinitials'] = ''
        blankRec['unfl'] = ''
        blankRec['unfl_per'] = 0
        blankRec['unat_non'] = ''
        blankRec['uanth_veg'] = ''
        blankRec['uanth_non'] = ''
        blankRec['umod1'] = ''
        blankRec['umod1_ext'] = 0
        blankRec['umod1_yr'] = 0
        blankRec['umod2'] = ''
        blankRec['umod2_ext'] = 0
        blankRec['umod2_yr'] = 0
        blankRec['udata'] = ''
        blankRec['udata_yr'] = 0
        
        return blankRec

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @staticmethod
    def fieldDisplayLength():
        '''
        Returns a dictionary keyed on field name that contains the
        display length of each field. Used to generate formatted text
        strings used to display AVI records.
        '''
        
        fieldLen = {} 
        fieldLen['poly_num'] = 10
        fieldLen['moist_reg'] = 1
        fieldLen['density'] = 1
        fieldLen['height'] = 2
        fieldLen['sp1'] = 2
        fieldLen['sp1_per'] = 2
        fieldLen['sp2'] = 2
        fieldLen['sp2_per'] = 1
        fieldLen['sp3'] = 2
        fieldLen['sp3_per'] = 1
        fieldLen['sp4'] = 2
        fieldLen['sp4_per'] = 1
        fieldLen['sp5'] = 2
        fieldLen['sp5_per'] = 1
        fieldLen['struc'] = 1
        fieldLen['struc_val'] = 2
        fieldLen['origin'] = 4
        fieldLen['tpr'] = 1
        fieldLen['initials'] = 2
        fieldLen['nfl'] = 2
        fieldLen['nfl_per'] = 2
        fieldLen['nat_non'] = 3
        fieldLen['anth_veg'] = 3
        fieldLen['anth_non'] = 3
        fieldLen['mod1'] = 2
        fieldLen['mod1_ext'] = 1
        fieldLen['mod1_yr'] = 4
        fieldLen['mod2'] = 2
        fieldLen['mod2_ext'] = 1
        fieldLen['mod2_yr'] = 4
        fieldLen['data'] = 1
        fieldLen['data_yr'] = 4
        fieldLen['umoist_reg'] = 1
        fieldLen['udensity'] = 1
        fieldLen['uheight'] = 2
        fieldLen['usp1'] = 2
        fieldLen['usp1_per'] = 2
        fieldLen['usp2'] = 2
        fieldLen['usp2_per'] = 1
        fieldLen['usp3'] = 2
        fieldLen['usp3_per'] = 1
        fieldLen['usp4'] = 2
        fieldLen['usp4_per'] = 1
        fieldLen['usp5'] = 2
        fieldLen['usp5_per'] = 1
        fieldLen['ustruc'] = 1
        fieldLen['ustruc_val'] = 2
        fieldLen['uorigin'] = 4
        fieldLen['utpr'] = 1
        fieldLen['uinitials'] = 2
        fieldLen['unfl'] = 2
        fieldLen['unfl_per'] = 2
        fieldLen['unat_non'] = 3
        fieldLen['uanth_veg'] = 3
        fieldLen['uanth_non'] = 3
        fieldLen['umod1'] = 2
        fieldLen['umod1_ext'] = 1
        fieldLen['umod1_yr'] = 4
        fieldLen['umod2'] = 2
        fieldLen['umod2_ext'] = 1
        fieldLen['umod2_yr'] = 4
        fieldLen['udata'] = 1
        fieldLen['udata_yr'] = 4
        
        return fieldLen

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @staticmethod
    def fieldNameList():
        '''
        Returns a list of AVI field names in the order is which they should
        positionaly appear in a typical AVI record. This is order stored in
        the Python Record field.
        '''
        
        fieldNameList = []
        fieldNameList.append('poly_num')
        fieldNameList.append('moist_reg')
        fieldNameList.append('density')
        fieldNameList.append('height')
        fieldNameList.append('sp1')
        fieldNameList.append('sp1_per')
        fieldNameList.append('sp2')
        fieldNameList.append('sp2_per')
        fieldNameList.append('sp3')
        fieldNameList.append('sp3_per')
        fieldNameList.append('sp4')
        fieldNameList.append('sp4_per')
        fieldNameList.append('sp5')
        fieldNameList.append('sp5_per')
        fieldNameList.append('struc')
        fieldNameList.append('struc_val')
        fieldNameList.append('origin')
        fieldNameList.append('tpr')
        fieldNameList.append('initials')
        fieldNameList.append('nfl')
        fieldNameList.append('nfl_per')
        fieldNameList.append('nat_non')
        fieldNameList.append('anth_veg')
        fieldNameList.append('anth_non')
        fieldNameList.append('mod1')
        fieldNameList.append('mod1_ext')
        fieldNameList.append('mod1_yr')
        fieldNameList.append('mod2')
        fieldNameList.append('mod2_ext')
        fieldNameList.append('mod2_yr')
        fieldNameList.append('data')
        fieldNameList.append('data_yr')
        fieldNameList.append('umoist_reg')
        fieldNameList.append('udensity')
        fieldNameList.append('uheight')
        fieldNameList.append('usp1')
        fieldNameList.append('usp1_per')
        fieldNameList.append('usp2')
        fieldNameList.append('usp2_per')
        fieldNameList.append('usp3')
        fieldNameList.append('usp3_per')
        fieldNameList.append('usp4')
        fieldNameList.append('usp4_per')
        fieldNameList.append('usp5')
        fieldNameList.append('usp5_per')
        fieldNameList.append('ustruc')
        fieldNameList.append('ustruc_val')
        fieldNameList.append('uorigin')
        fieldNameList.append('utpr')
        fieldNameList.append('uinitials')
        fieldNameList.append('unfl')
        fieldNameList.append('unfl_per')
        fieldNameList.append('unat_non')
        fieldNameList.append('uanth_veg')
        fieldNameList.append('uanth_non')
        fieldNameList.append('umod1')
        fieldNameList.append('umod1_ext')
        fieldNameList.append('umod1_yr')
        fieldNameList.append('umod2')
        fieldNameList.append('umod2_ext')
        fieldNameList.append('umod2_yr')
        fieldNameList.append('udata')
        fieldNameList.append('udata_yr')
        
        return fieldNameList

# ----------------------------------------------------------
class AVI_Record(object):
    '''
    Provides function to operate on individual AVI records
    contained in a table or feature class
    '''

    # Case sensitive domain lists for AVI fields.
    domainSpeciesList = ['Sw','Sb','Se','P','Pl','Pj','Pa','Pf','Fb','Fd','Fa','Lt','La','Lw','Aw','Bw','Pb','A','']
    domainMoistureRegimeList = ['d','m','w','a','']
    domainTPRList = ['G','M','F','U','']
    domainCrownClosureList = ['A','B','C','D','']
    domainStandStructureList = ['M','H','C','']
    domainDataSourceList = ['F','P','V','C','S','A','L','I','']
    domainNonForestVegList = ['SC','SO','HG','HF','BR','']
    domainNatNonVegList = ['NWI','NWL','NWR','NWF','NMB','NMC','NMR','NMS','']
    domainAnthVegList = ['CA','CP','CPR','CIP','CIW','']
    domainAnthNonVegList = ['ASC','ASR','AIH','AIE','AIG','AIF','AIM','AII','']
    domainModifierList = ['CC','BU','WF','CL','DI','IK','UK','WE','DT','BT','SN','ST','SI','SC','PL','TH','GR','IR','']
    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, featureClass):
        '''
        Attributes:
            fcPath - path to the table or feature class
            fcName - name of table or feature class
            OIDFieldName - name of OID field if one exists otherwise ''
            fieldNames - lowercase list of all field names excluding
                         shapefield and any BLOB fields.
            fieldTypes - dictionary of all field types listed in
                         field names. The key for the dictionary is
                         the lowercase name for the field.
            fieldValues - dictionary of field values. Key for dictionary
                          is the lowercase field name. When a record is
                          read this dictionary is populated with values.
                          By default any NULL values will be converted to
                          an empty string '' or 0 depending on the field type.
            speciesData - List contains series of lists with first item in
                          list the species and the second item its percentage.
                          Example:
                              (('Sw',6),('Aw',4),('',0),('',0),('',0))

            allFieldNames - list containing names of all AVI 2.1 fields
            overstoryFieldNames - list of all AVI 2.1 field names for the
                                  overstory portion of record.

            understoryFieldNames - list of all AVI 2.1 field names for the
                                   understory portion of record.
            
        '''

        self._logChn = srd_logging.SRD_Log()        

        if not arcpy.Exists(featureClass):
            raise SRD_Exception('Feature class: %s not found' % featureClass)

        # May have feature layer passed so parse out.
        utilFCObj = srd_featureclass_info.SRD_FeatureClassInfo(featureClass)
        self.fcPath = utilFCObj.fullPath
        self.fcName = os.path.basename(self.fcPath)

        # May need OID for reference.
        # Require OID field name to prevent writing or field values.
        if arcpy.Describe(featureClass).HasOID:
            self.OIDFieldName = arcpy.Describe(featureClass).OIDFieldName.lower()
        else:
            self.OIDFieldName = ''
        
        # Build list of field names that will be used to read attribute data.
        self.fieldNames = []
        # Save the field types for each field in a dictionary with
        # key based on field name.
        self.fieldTypes = {}
        
        # Field values read from record will be stored in dictionary.
        # See redRecord for details.
        self.fieldValues = {}

        self.speciesData = ()

        # The attribute key ID is used to identify identical attribute
        # records and assign a key ID. This ID then can be used as a
        # dissolve field to remove township boundaries.
        self.attributeKeyIDFieldName = 'att_key_id'
        self.attributeKeyFlagFieldName = 'att_key_flag'
        
        # Define overstory fields.
        overstoryFieldNames = []
        overstoryFieldNames.append('moist_reg')
        overstoryFieldNames.append('density')
        overstoryFieldNames.append('height')
        overstoryFieldNames.append('sp1')
        overstoryFieldNames.append('sp1_per')
        overstoryFieldNames.append('sp2')
        overstoryFieldNames.append('sp2_per')
        overstoryFieldNames.append('sp3')
        overstoryFieldNames.append('sp3_per')
        overstoryFieldNames.append('sp4')
        overstoryFieldNames.append('sp4_per')
        overstoryFieldNames.append('sp5')
        overstoryFieldNames.append('sp5_per')
        overstoryFieldNames.append('struc')
        overstoryFieldNames.append('struc_val')
        overstoryFieldNames.append('origin')
        overstoryFieldNames.append('tpr')
        overstoryFieldNames.append('initials')
        overstoryFieldNames.append('nfl')
        overstoryFieldNames.append('nfl_per')
        overstoryFieldNames.append('nat_non')
        overstoryFieldNames.append('anth_veg')
        overstoryFieldNames.append('anth_non')
        overstoryFieldNames.append('mod1')
        overstoryFieldNames.append('mod1_ext')
        overstoryFieldNames.append('mod1_yr')
        overstoryFieldNames.append('mod2')
        overstoryFieldNames.append('mod2_ext')
        overstoryFieldNames.append('mod2_yr')
        overstoryFieldNames.append('data')
        overstoryFieldNames.append('data_yr')

        self.overstoryFieldNames = overstoryFieldNames[:]
        
        # Define understory fields.
        understoryFieldNames = []
        understoryFieldNames.append('umoist_reg')
        understoryFieldNames.append('udensity')
        understoryFieldNames.append('uheight')
        understoryFieldNames.append('usp1')
        understoryFieldNames.append('usp1_per')
        understoryFieldNames.append('usp2')
        understoryFieldNames.append('usp2_per')
        understoryFieldNames.append('usp3')
        understoryFieldNames.append('usp3_per')
        understoryFieldNames.append('usp4')
        understoryFieldNames.append('usp4_per')
        understoryFieldNames.append('usp5')
        understoryFieldNames.append('usp5_per')
        understoryFieldNames.append('ustruc')
        understoryFieldNames.append('ustruc_val')
        understoryFieldNames.append('uorigin')
        understoryFieldNames.append('utpr')
        understoryFieldNames.append('uinitials')
        understoryFieldNames.append('unfl')
        understoryFieldNames.append('unfl_per')
        understoryFieldNames.append('unat_non')
        understoryFieldNames.append('uanth_veg')
        understoryFieldNames.append('uanth_non')
        understoryFieldNames.append('umod1')
        understoryFieldNames.append('umod1_ext')
        understoryFieldNames.append('umod1_yr')
        understoryFieldNames.append('umod2')
        understoryFieldNames.append('umod2_ext')
        understoryFieldNames.append('umod2_yr')
        understoryFieldNames.append('udata')
        understoryFieldNames.append('udata_yr')

        self.understoryFieldNames = understoryFieldNames[:]

        # May have some extensions to normal AVI field names.
        extendedFieldNames = []
        extendedFieldNames.append('photo_yr')
        extendedFieldNames.append('aris')
        extendedFieldNames.append('dist_ptrn')
        extendedFieldNames.append('lid_und')

        self.extendedFieldNames = extendedFieldNames[:]

        # Combination of all fields.
        self.allFieldNames = []
        self.allFieldNames.append('poly_num')
        self.allFieldNames += overstoryFieldNames
        self.allFieldNames += understoryFieldNames
        self.allFieldNames += extendedFieldNames

        # Obtain list of fields that will be stored in the record.
        # This list also represents order for comma delimited fields
        # that will be created.
        self.AVIFieldNameList = AVI_RecordUtils.fieldNameList()

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def returnLayerRecord(self, layer, rec = None):
        '''
        The standard AVI 2.1 record really consists of two records,
        one for the overstory and one for the understory. In some
        circumstances it is easier to focus on or the other layer
        when performing tasks such as attribute validation. Rather
        than having two identical routines for the overstory and understory
        it is easier to pass the overstory and understory records
        seperately to a common routine that uses the overstory field
        names to obtain required information. This routine will return
        such a record.
        
        Parameters:

            layer - layer to return. Either OVER or UNDER to represent
                    the overstory layer or the understory layer.
                    
            rec - dictionary containing AVI 2.1 fields. Note that this
                  must have a key using the lowercase names for the AVI
                  fields. The record can contain additional non-AVI fields
                  and these will be simply passed thru to swapped record as
                  is. This is an optional parameter and if not passed then
                  the current fieldValues for the record will be used.

        '''

        layerRec = {}

        if not rec:
            rec = self.fieldValues

        if layer.upper() == 'OVER':
            for fieldName in self.overstoryFieldNames:
                if rec.has_key(fieldName):
                    layerRec[fieldName] = rec[fieldName]

        elif layer.upper() == 'UNDER':
            for fieldName in self.overstoryFieldNames:
                if rec.has_key(fieldName):
                    uFieldName = 'u%s' % fieldName
                    layerRec[fieldName] = rec[uFieldName]
        else:
            raise SRD_Exception('Invalid layer type: %s passed to returnLayerRecord' % layer)

        # Always return POLY_NUM and OID
        # If there is no such field then return a -1 to indicate error.
        if rec.has_key('poly_num'):
            layerRec['poly_num'] = rec['poly_num']
        else:
            layerRec['poly_num'] = -1

        if rec.has_key('oid'):
            layerRec['oid'] = rec['oid']
        else:
            layerRec['oid'] = -1

        if rec.has_key('photo_yr'):
            layerRec['photo_yr'] = rec['photo_yr']
        else:
            layerRec['photo_yr'] = 0

        if rec.has_key('aris'):
            layerRec['aris'] = rec['aris']
        else:
            layerRec['aris'] = ''

        return layerRec

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def blankoutUnderstory(self):
        '''
        Clears the understory record by setting values to empty string
        or 0
        '''
        
        self.fieldValues['umoist_reg'] = ''
        self.fieldValues['udensity'] = ''
        self.fieldValues['uheight'] = 0
        self.fieldValues['usp1'] = ''
        self.fieldValues['usp1_per'] = 0
        self.fieldValues['usp2'] = ''
        self.fieldValues['usp2_per'] = 0
        self.fieldValues['usp3'] = ''
        self.fieldValues['usp3_per'] = 0
        self.fieldValues['usp4'] = ''
        self.fieldValues['usp4_per'] = 0
        self.fieldValues['usp5'] = ''
        self.fieldValues['usp5_per'] = 0
        self.fieldValues['ustruc'] = ''
        self.fieldValues['ustruc_val'] = 0
        self.fieldValues['uorigin'] = 0
        self.fieldValues['utpr'] = ''
        self.fieldValues['uinitials'] = ''
        self.fieldValues['unfl'] = ''
        self.fieldValues['unfl_per'] = 0
        self.fieldValues['unat_non'] = ''
        self.fieldValues['uanth_veg'] = ''
        self.fieldValues['uanth_non'] = ''
        self.fieldValues['umod1'] = ''
        self.fieldValues['umod1_ext'] = 0
        self.fieldValues['umod1_yr'] = 0
        self.fieldValues['umod2'] = ''
        self.fieldValues['umod2_ext'] = 0
        self.fieldValues['umod2_yr'] = 0
        self.fieldValues['udata'] = ''
        self.fieldValues['udata_yr'] = 0

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def blankoutOverstory(self):
        '''
        Clears the overstory record by setting values to empty string
        or 0
        '''
        
        self.fieldValues['moist_reg'] = ''
        self.fieldValues['density'] = ''
        self.fieldValues['height'] = 0
        self.fieldValues['sp1'] = ''
        self.fieldValues['sp1_per'] = 0
        self.fieldValues['sp2'] = ''
        self.fieldValues['sp2_per'] = 0
        self.fieldValues['sp3'] = ''
        self.fieldValues['sp3_per'] = 0
        self.fieldValues['sp4'] = ''
        self.fieldValues['sp4_per'] = 0
        self.fieldValues['sp5'] = ''
        self.fieldValues['sp5_per'] = 0
        self.fieldValues['struc'] = ''
        self.fieldValues['struc_val'] = 0
        self.fieldValues['origin'] = 0
        self.fieldValues['tpr'] = ''
        self.fieldValues['initials'] = ''
        self.fieldValues['nfl'] = ''
        self.fieldValues['nfl_per'] = 0
        self.fieldValues['nat_non'] = ''
        self.fieldValues['anth_veg'] = ''
        self.fieldValues['anth_non'] = ''
        self.fieldValues['mod1'] = ''
        self.fieldValues['mod1_ext'] = 0
        self.fieldValues['mod1_yr'] = 0
        self.fieldValues['mod2'] = ''
        self.fieldValues['mod2_ext'] = 0
        self.fieldValues['mod2_yr'] = 0
        self.fieldValues['data'] = ''
        self.fieldValues['data_yr'] = 0

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def swapOverUnderStory(self, rec = None):
        '''
        Will swap the values in the overstory for those in the
        unerstory. This is intended to be used when you are dealing
        with horizontal stands or A density overstories where you
        with to consider the understory as the dominant layer. In
        this way you do not have to add additional logic to process the
        understory. Perfrom a swap then send to normal processing so that
        sp1 will really represent usp1 etc..

        Parameters:
            rec - dictionary containing AVI 2.1 fields. Note that this
                  must have a key using the lowercase names for the AVI
                  fields. The record can contain additional non-AVI fields
                  and these will be simply passed thru to swapped record as
                  is. This is an optional parameter and if not passed then
                  the current fieldValues for the record will be used.

        '''

        newRec = {}

        if not rec:
            rec = self.fieldValues
            
        # First copy all overstory fields to the understory.
        for fieldName in self.overstoryFieldNames:
            if fieldName in rec:
                uFieldName = 'u%s' % fieldName
                newRec[uFieldName] = rec[fieldName]

        # Next copy all understory fields to the overstory.
        for fieldName in self.understoryFieldNames:
            if fieldName in rec:
                oFieldName = fieldName[1:]
                newRec[oFieldName] = rec[fieldName]

        # Add on remaining fields from the record so no data is lost.
        # For the basic AVI record this will include the poly_num but
        # for all other records it will include any additional attributes
        # not covered by AVI 2.1
        for key,val in rec.items():
            if not key in newRec:
                newRec[key] = val

        # Do not modify the existing attributes as stored in fieldValues
        # but return the swapped record so it can be used independent of
        # original values.
        return newRec
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def containsCoreData(self):
        '''
        Determines if the AVI record contains any core data.

        An AVI record can describe various types of land cover for a
        stand such as treed vegetation or naturally non-vegetated
        land. Before an AVI record can be used it must contain data for
        at least one type of land cover. This function determines if an
        AVI record has any land cover data associated with it.
        
        Core data requires that one of the following fields  
        be populated to be considered a legitimate AVI record:
            SP1 (forested land)
            NFL (non-forested land)
            NAT_NON (naturally non-vegetated
            ANTH_VEG (anthropogenic vegetated)
            ANTH_NON (anthropogenic non-vegetated)
            
        Will return True if core data detected.
        '''
        
        return AVI_RecordUtils.containsCoreData(self.fieldValues)
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def containsCoreUnderstoryData(self):
        '''
        Determines if the AVI record contains any core data in understory.

        An AVI record can describe various types of land cover for a
        stand such as treed vegetation or naturally non-vegetated
        land. Before an AVI record can be used it must contain data for
        at least one type of land cover. This function determines if an
        AVI record has any land cover data associated with it.
        
        Core data requires that one of the following fields  
        be populated to be considered a legitimate AVI record:
            USP1 (forested land)
            UNFL (non-forested land)
            UNAT_NON (naturally non-vegetated
            UANTH_VEG (anthropogenic vegetated)
            UANTH_NON (anthropogenic non-vegetated)
            
        Will return True if core data detected.
        '''
        
        return AVI_RecordUtils.containsCoreUnderstoryData(self.fieldValues)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def flagCoreData(self, flagFieldName = 'CORE_AVI'):
        '''
        This method will flag polygons to indicate if they
        contain core AVI data. A SHORT integer field is added to
        the AVI feature class. If the polygon contains core AVI
        attributes it is assigned a value of 1, otherwise the value
        will be 0.

        Parameters:
            flagFieldName - name of field you wish to flag core AVI
                            status. Defaults to CORE_AVI.
        '''

        self._logChn.logMsg('\n***\n*** Validating Core AVI...\n***')

        if not arcpy.ListFields(self.fcPath, flagFieldName):
            arcpy.AddField_management(self.fcPath, flagFieldName.upper(), 'SHORT')

        self._logChn.logMsg('\n***\n*** Field: %s Used To Flag Core AVI...\n***' % flagFieldName)

        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Flagging core data...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

        # Define minimum list of attributes to determine core AVI.
        fieldList = ['sp1', 'nfl', 'nat_non', 'anth_veg', 'anth_non', 'struc', 
                     'usp1', 'unfl', 'unat_non', 'uanth_veg', 'uanth_non']
        
        # Add the Flag field and obtain its index to enable uupdate.
        fieldList.append(flagFieldName.lower())
        flagFieldIdx = fieldList.index(flagFieldName.lower())
        
        errCnt = 0
        uerrCnt = 0
        with arcpy.da.UpdateCursor(self.fcPath, fieldList) as cursor:
            for row in cursor:

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)

                if self.containsCoreData():
                    flagVal = 1
                    # If multi-storied then check understory.
                    if self.fieldValues['struc'].upper() in ('M', 'H'):
                        if self.containsCoreUnderstoryData():
                            flagVal = 1
                        else:
                            flagVal = -1
                            uerrCnt += 1
                            errCnt += 1
                else:
                    flagVal = 0
                    errCnt += 1

                row[flagFieldIdx] = flagVal
                cursor.updateRow(row)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Core AVI validation completed, %s errors detected\n***' % errCnt)
        if errCnt > 0:
            self._logChn.logMsg('   CORE_AVI =  1 - stand has core AVI data')
            self._logChn.logMsg('   CORE_AVI =  0 - stand missing core AVI data')
            self._logChn.logMsg('   CORE_AVI = -1 - stand is M or H and understory missing core AVI data (%s detected)' % uerrCnt)
            
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def setProperCaseForCurrentRecord(self):
        '''
        When dealing with AVI attributes it is sometimes important
        to have values in the proper case. For instance, a species
        attribute should always start with a capital letter followed
        by a lowercase letter. This method will take the current AVI
        and place it in its proper case.
        '''

        self.fieldValues['moist_reg'] = self.fieldValues['moist_reg'].lower()
        self.fieldValues['density'] = self.fieldValues['density'].upper()
        self.fieldValues['sp1'] = self.fieldValues['sp1'].title()
        self.fieldValues['sp2'] = self.fieldValues['sp2'].title()
        self.fieldValues['sp3'] = self.fieldValues['sp3'].title()
        self.fieldValues['sp4'] = self.fieldValues['sp4'].title()
        self.fieldValues['sp5'] = self.fieldValues['sp5'].title()
        self.fieldValues['struc'] = self.fieldValues['struc'].upper()
        self.fieldValues['tpr'] = self.fieldValues['tpr'].upper()
        self.fieldValues['nfl'] = self.fieldValues['nfl'].upper()
        self.fieldValues['nat_non'] = self.fieldValues['nat_non'].upper()
        self.fieldValues['anth_veg'] = self.fieldValues['anth_veg'].upper()
        self.fieldValues['anth_non'] = self.fieldValues['anth_non'].upper()
        self.fieldValues['mod1'] = self.fieldValues['mod1'].upper()
        self.fieldValues['mod2'] = self.fieldValues['mod2'].upper()
        self.fieldValues['data'] = self.fieldValues['data'].upper()

        self.fieldValues['umoist_reg'] = self.fieldValues['umoist_reg'].lower()
        self.fieldValues['udensity'] = self.fieldValues['udensity'].upper()
        self.fieldValues['usp1'] = self.fieldValues['usp1'].title()
        self.fieldValues['usp2'] = self.fieldValues['usp2'].title()
        self.fieldValues['usp3'] = self.fieldValues['usp3'].title()
        self.fieldValues['usp4'] = self.fieldValues['usp4'].title()
        self.fieldValues['usp5'] = self.fieldValues['usp5'].title()
        self.fieldValues['ustruc'] = self.fieldValues['ustruc'].upper()
        self.fieldValues['utpr'] = self.fieldValues['utpr'].upper()
        self.fieldValues['unfl'] = self.fieldValues['unfl'].upper()
        self.fieldValues['unat_non'] = self.fieldValues['unat_non'].upper()
        self.fieldValues['uanth_veg'] = self.fieldValues['uanth_veg'].upper()
        self.fieldValues['uanth_non'] = self.fieldValues['uanth_non'].upper()
        self.fieldValues['umod1'] = self.fieldValues['umod1'].upper()
        self.fieldValues['umod2'] = self.fieldValues['umod2'].upper()
        self.fieldValues['udata'] = self.fieldValues['udata'].upper()

        if 'lid_und' in self.fieldValues:
            self.fieldValues['lid_und'] = self.fieldValues['lid_und'].upper()

        if 'aris' in self.fieldValues:
            self.fieldValues['aris'] = self.fieldValues['aris'].upper()
            
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def setProperCase(self):
        '''
        Sets the proper case for all attributes in the current
        feature class associated with the instance.

        '''

        # List of fields that will be processed for proper case.
        fieldList = ['moist_reg','density','sp1','sp2','sp3','sp4','sp5', 
                    'struc','tpr','nfl','nat_non','anth_veg','anth_non', 
                    'mod1','mod2','data', 
                    'umoist_reg','udensity','usp1','usp2','usp3','usp4','usp5', 
                    'ustruc','utpr','unfl','unat_non','uanth_veg','uanth_non', 
                    'umod1','umod2','udata', 'lid_und', 'aris']

        self._logChn.logMsg('\n***\n*** Setting proper case of AVI attributes...\n***')

        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Setting proper case...', 0, selCnt, 1)
        
        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

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

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)

                self.setProperCaseForCurrentRecord()
                
                # Update row with proper case values.
                for idx in range(0, len(fieldList)):
                    fieldName = fieldList[idx]
                    row[idx] = self.fieldValues[fieldName]

                cursor.updateRow(row)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Proper case completed processing\n***')
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def removeBlanks(self):
        '''
        Removes all leading and trailing blanks from the character
        fields. In some cases data derived from coverages could have
        imbeded blanks. For instance, a moisture regime field that has
        no call will contain ' ' rather than an emptyr string ''. This
        can cause problems when querying or analyzing the data. For
        consistancy all character fields that do not have a value will
        be assigned an empty string ''.
        '''

        # List of fields that will be processed for proper case.
        fieldList = ['moist_reg','density','sp1','sp2','sp3','sp4','sp5', 
                    'struc','tpr','nfl','nat_non','anth_veg','anth_non', 
                    'mod1','mod2','data', 
                    'umoist_reg','udensity','usp1','usp2','usp3','usp4','usp5', 
                    'ustruc','utpr','unfl','unat_non','uanth_veg','uanth_non', 
                    'umod1','umod2','udata', 'lid_und', 'aris']

        self._logChn.logMsg('\n***\n*** Removing blanks from AVI attributes...\n***')

        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Removing blanks...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

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

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=False, stripBlanks=True)

                # Update row with proper case values.
                for idx in range(0, len(fieldList)):
                    fieldName = fieldList[idx]
                    row[idx] = self.fieldValues[fieldName]

                cursor.updateRow(row)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Blank removal completed\n***')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def removeNulls(self):
        '''
        Removes all NULLs from raditiona set of AVI attributes and
        replaces them with empty string if text field or 0 if a numeric
        field.
        '''

        # List of fields that will be processed for proper case.

        self._logChn.logMsg('\n***\n*** Removing NULLs from AVI attributes...\n***')

        # Will only update traditional AVI field values.
        fieldList = self.allFieldNames

        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Replacing NULL values...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

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

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=False)

                # Update row with proper case values.
                for idx in range(0, len(fieldList)):
                    fieldName = fieldList[idx]
                    row[idx] = self.fieldValues[fieldName]

                cursor.updateRow(row)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()
        
        self._logChn.logMsg('\n***\n*** NULL removal completed\n***')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def speciesDataList(self, standLayer):
        '''
        Build list containing species data and assigns to the
        attribute speciesData.

        Many AVI functions rely on the species composition of
        a particular stand layer to perform calculations. This
        function will generate a structure that contains the species
        composition for a particular stand layer.

        List contains series of lists with first item in list the
        species and the second item its percentage.
        
        Example: (('Sw',6),('Aw',4),('',0),('',0),('',0))

        Parameters:
            standLayer - (OVERSTORY/UNDERSTORY)
                Either the species information for the overstory or understory
                is returned. Set standLayer to either OVERSTORY or UNDERSTORY
                to indicate the layer species information to return.
        '''

        self.speciesData = AVI_RecordUtils.speciesDataList(self.fieldValues, standLayer)
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateDomainsForCurrentRecord(self):
        '''
        Will validate the coded domain values for AVI attributes. The
        coded domains are those fields with set code values that can be
        referenced in a look-up table (example Sp1 field) This routine
        will validate domains for the current AVI record.
        '''

        errorList = []

        if not self.fieldValues['moist_reg'] in self.domainMoistureRegimeList:
            msg = 'Invalid moist_reg: %s' % self.fieldValues['moist_reg']
            errorList.append(msg)
            
        if not self.fieldValues['density'] in self.domainCrownClosureList:
            msg = 'Invalid density: %s' % self.fieldValues['density']
            errorList.append(msg)

        if not self.fieldValues['sp1'] in self.domainSpeciesList:
            msg = 'Invalid sp1: %s' % self.fieldValues['sp1']
            errorList.append(msg)

        if not self.fieldValues['sp2'] in self.domainSpeciesList:
            msg = 'Invalid sp2: %s' % self.fieldValues['sp2']
            errorList.append(msg)

        if not self.fieldValues['sp3'] in self.domainSpeciesList:
            msg = 'Invalid sp3: %s' % self.fieldValues['sp3']
            errorList.append(msg)

        if not self.fieldValues['sp4'] in self.domainSpeciesList:
            msg = 'Invalid sp4: %s' % self.fieldValues['sp4']
            errorList.append(msg)

        if not self.fieldValues['sp5'] in self.domainSpeciesList:
            msg = 'Invalid sp5: %s' % self.fieldValues['sp5']
            errorList.append(msg)
            
        if not self.fieldValues['struc'] in self.domainStandStructureList:
            msg = 'Invalid struc: %s' % self.fieldValues['struc']
            errorList.append(msg)
            
        if not self.fieldValues['tpr'] in self.domainTPRList:
            msg = 'Invalid tpr: %s' % self.fieldValues['tpr']
            errorList.append(msg)
        
        if not self.fieldValues['nfl'] in self.domainNonForestVegList:
            msg = 'Invalid nfl: %s' % self.fieldValues['nfl']
            errorList.append(msg)
            
        if not self.fieldValues['nat_non'] in self.domainNatNonVegList:
            msg = 'Invalid nat_non: %s' % self.fieldValues['nat_non']
            errorList.append(msg)
            
        if not self.fieldValues['anth_veg'] in self.domainAnthVegList:
            msg = 'Invalid anth_veg: %s' % self.fieldValues['anth_veg']
            errorList.append(msg)
            
        if not self.fieldValues['anth_non'] in self.domainAnthNonVegList:
            msg = 'Invalid anth_non: %s' % self.fieldValues['anth_non']
            errorList.append(msg)
            
        if not self.fieldValues['mod1'] in self.domainModifierList:
            msg = 'Invalid mod1: %s' % self.fieldValues['mod1']
            errorList.append(msg)

        if not self.fieldValues['mod2'] in self.domainModifierList:
            msg = 'Invalid mod2: %s' % self.fieldValues['mod2']
            errorList.append(msg)
            
        if not self.fieldValues['data'] in self.domainDataSourceList:
            msg = 'Invalid data: %s' % self.fieldValues['data']
            errorList.append(msg)


        if not self.fieldValues['umoist_reg'] in self.domainMoistureRegimeList:
            msg = 'Invalid umoist_reg: %s' % self.fieldValues['umoist_reg']
            errorList.append(msg)
            
        if not self.fieldValues['udensity'] in self.domainCrownClosureList:
            msg = 'Invalid udensity: %s' % self.fieldValues['udensity']
            errorList.append(msg)

        if not self.fieldValues['usp1'] in self.domainSpeciesList:
            msg = 'Invalid usp1: %s' % self.fieldValues['usp1']
            errorList.append(msg)

        if not self.fieldValues['usp2'] in self.domainSpeciesList:
            msg = 'Invalid usp2: %s' % self.fieldValues['usp2']
            errorList.append(msg)

        if not self.fieldValues['usp3'] in self.domainSpeciesList:
            msg = 'Invalid usp3: %s' % self.fieldValues['usp3']
            errorList.append(msg)

        if not self.fieldValues['usp4'] in self.domainSpeciesList:
            msg = 'Invalid usp4: %s' % self.fieldValues['usp4']
            errorList.append(msg)

        if not self.fieldValues['usp5'] in self.domainSpeciesList:
            msg = 'Invalid usp5: %s' % self.fieldValues['usp5']
            errorList.append(msg)
            
        if not self.fieldValues['ustruc'] in self.domainStandStructureList:
            msg = 'Invalid ustruc: %s' % self.fieldValues['ustruc']
            errorList.append(msg)
            
        if not self.fieldValues['utpr'] in self.domainTPRList:
            msg = 'Invalid utpr: %s' % self.fieldValues['utpr']
            errorList.append(msg)
        
        if not self.fieldValues['unfl'] in self.domainNonForestVegList:
            msg = 'Invalid unfl: %s' % self.fieldValues['unfl']
            errorList.append(msg)
            
        if not self.fieldValues['unat_non'] in self.domainNatNonVegList:
            msg = 'Invalid unat_non: %s' % self.fieldValues['unat_non']
            errorList.append(msg)
            
        if not self.fieldValues['uanth_veg'] in self.domainAnthVegList:
            msg = 'Invalid uanth_veg: %s' % self.fieldValues['uanth_veg']
            errorList.append(msg)
            
        if not self.fieldValues['uanth_non'] in self.domainAnthNonVegList:
            msg = 'Invalid uanth_non: %s' % self.fieldValues['uanth_non']
            errorList.append(msg)
            
        if not self.fieldValues['umod1'] in self.domainModifierList:
            msg = 'Invalid umod1: %s' % self.fieldValues['umod1']
            errorList.append(msg)

        if not self.fieldValues['umod2'] in self.domainModifierList:
            msg = 'Invalid umod2: %s' % self.fieldValues['umod2']
            errorList.append(msg)
            
        if not self.fieldValues['udata'] in self.domainDataSourceList:
            msg = 'Invalid udata: %s' % self.fieldValues['udata']
            errorList.append(msg)

        return errorList
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateDomains(self):
        '''
        Will validate the coded domain values for AVI attributes. The
        coded domains are those fields with set code values that can be
        referenced in a look-up table (example Sp1 field) This routine
        will validate domains for the entire AVI feature class that is
        associated with the current instance of this class.

        To hold any error messages a field named DOM_ERRORS will be added
        to the feature class. Any errors detected will be concatenated
        togeather and assigned to this field. If more than one error is
        encountered the messages will be seperated with a | character.
        
        '''

        self._logChn.logMsg('\n***\n*** Validating AVI Domains...\n***')

        if not arcpy.ListFields(self.fcPath, 'dom_errors'):
            self._logChn.logMsg('\n*** Adding dom_errors')
            arcpy.AddField_management(self.fcPath, 'dom_errors', 'TEXT', '', '',1000)

        # Will only update traditional AVI field values.
        fieldList = self.allFieldNames
        fieldList.append('dom_errors')
        domErrIdx = fieldList.index('dom_errors')
        
        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Validating domains...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

        errCnt = 0
        with arcpy.da.UpdateCursor(self.fcPath, fieldList) as cursor:
            for row in cursor:

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)

                errorList = self.validateDomainsForCurrentRecord()
                errMsg = ''
                if errorList:
                    errCnt += 1
                    for domError in errorList:
                        if errMsg:
                            errMsg += ' | ' + domError
                        else:
                            errMsg += domError

                # Perform update if any errors detected.
                row[domErrIdx] = errMsg
                cursor.updateRow(row)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Domain validation completed, %s errors detected\n***' % errCnt)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildAttributeKey(self, excludeFieldList = [], excludeCC=False):
        '''
        This method will add an ID field to the feature class and calculate this
        value based on the AVI record values. Each AVI record is converted to a
        string and this string is used as a key in a dictionary. If the key is
        already in the dicitonary then it means that this AVI record has the same
        value as a previous record and it is assigned the ID of this previous record.
        This can then be used to perform a dissolve of the polygons based on the
        AVI record values. The resulting dissolved polygons can be linked back to
        their original attributes using this ID as the join item.

        Note that we also will flag the record where the first occurence of
        a unique record is encounterd. The first occurance will have the flag
        value set to 1 while all subsequent occurances will get a value of 0.

        Parameters:
            excludeFieldList - list of AVI field names to exclude from the
                               comparison. Normally 'initials' and 'uinitials'
                               should be ignored when performing the comparison
                               since these are essentially metadata fields.
        '''

        self._logChn.logMsg('\n***\n*** Building Attribute Key...\n***')

        if not arcpy.ListFields(self.fcPath, self.attributeKeyIDFieldName):
            self._logChn.logMsg('\n*** Adding attribute key ID field: %s' % self.attributeKeyIDFieldName)
            arcpy.AddField_management(self.fcPath, self.attributeKeyIDFieldName, 'LONG')

        # Flag the first occurance of record key so we can generate unique
        # set of AVI records based on key.
        if not arcpy.ListFields(self.fcPath, self.attributeKeyFlagFieldName):
            self._logChn.logMsg('\n*** Adding attribute key flag field: %s' % self.attributeKeyFlagFieldName)
            arcpy.AddField_management(self.fcPath, self.attributeKeyFlagFieldName, 'LONG')

        # Use the overstory and understory records excluding POLY_NUM
        attFieldNames = []
        attFieldNames += self.overstoryFieldNames[:]
        attFieldNames += self.understoryFieldNames[:]

        # May be dealing with extended attributes.
        # If they exist in the dataset then use.
        for extFieldName in self.extendedFieldNames:
            if arcpy.ListFields(self.fcPath, extFieldName):
                attFieldNames.append(extFieldName)
            
        # Remove any fields that are to be excluded from comparison.
        for fieldName in excludeFieldList:
            fieldName = fieldName.lower()
            if fieldName in attFieldNames:
                attFieldNames.remove(fieldName)

        attKeyIDFieldName = self.attributeKeyIDFieldName
        attKeyFlagFieldName = self.attributeKeyFlagFieldName

        # Field list is all fields that will be read or written.
        fieldList = attFieldNames[:]        
        fieldList.append(attKeyIDFieldName)
        fieldList.append(attKeyFlagFieldName)

        # Get index for fields that will be updated.
        attKeyIDIdx = fieldList.index(attKeyIDFieldName)
        attKeyFlagIdx = fieldList.index(attKeyFlagFieldName)
        
        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Building Attribute Key...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

        # Maintain a dictionary using the string value of the record as
        # a key with the attribute ID as is value.
        attSeqID = 0
        attIDDict = {}
        ccCounter = 0
        with arcpy.da.UpdateCursor(self.fcPath, fieldList) as cursor:
            for row in cursor:

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)

                # Build the attribute key using each value of the AVI record
                attKey = ''
                for fieldName in attFieldNames:
                    attKey += str(self.fieldValues[fieldName])

                # Where we wish to ignore CC from an attribute dissolve then
                # must make each record with a CC unique. A sequential number
                # will be added to any record with a CC so it will not be dissolved
                # with a neighbour.
                makeUnique = False
                if excludeCC:
                    if 'mod1' in self.fieldValues:
                        if self.fieldValues['mod1'] == 'CC':
                            makeUnique = True
                    if 'mod2' in self.fieldValues:
                        if self.fieldValues['mod2'] == 'CC':
                            makeUnique = True
                    if 'umod1' in self.fieldValues:
                        if self.fieldValues['umod1'] == 'CC':
                            makeUnique = True
                    if 'umod2' in self.fieldValues:
                        if self.fieldValues['umod2'] == 'CC':
                            makeUnique = True

                    # The exception to not dissolving a CC is when there is an associated
                    # ARIS number for the stand. In this case we can dissolve it.
                    if 'aris' in self.fieldValues:
                        if self.fieldValues['aris']:
                            makeUnique = False
                        
                if makeUnique:
                    ccCounter += 1
                    attKey += str(ccCounter)
                    
                # See if the current key is already in our dictionary. If
                # it is, then use that ID otherwise add it to our dictionary.
                if attKey in attIDDict:
                    attID = attIDDict[attKey]
                    attFlag = 0
                else:
                    attSeqID += 1
                    attID = attSeqID
                    attIDDict[attKey] = attSeqID
                    attFlag = 1

                row[attKeyIDIdx] = attID
                row[attKeyFlagIdx] = attFlag

                cursor.updateRow(row)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Attribute key built\n***')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def dissolveByAttributeKey(self, outFC, ignoreTPR, excludeCC=False, excludePhotoYr=False):
        '''
        This method will dissolve the AVI feature class based on all the
        AVI attributes (excluding overstory/understory initials and tpr) It is
        designed to be used to eliminate the township line found in most
        AVI datasets.
        
        The dissolve is dependent on building a unique set of AVI records for
        the entire feature class and assigning an ID based on this unique key
        that can be used for a dissolve. For example, all stands that are coded as NWL
        will be assigned the same keyID.

        The unique set of AVI records are exported to a tempoary table with
        the keyID as the linkage to all mach AVI records in the feature class.
        A dissolve is performed on the keyID and then the attribute table is
        linked back to the dissolved features using this keyID.

        Parameters:
            outFC - output feature class to create through the dissolve process.
        
        '''

        # Build the attribute key that will be used to perform the dissolve.
        # Do not use initials as part or our comparison key.
        excludeFieldNameList = ['initials', 'uinitials', 'feat_id']
        if ignoreTPR:
            excludeFieldNameList += ['tpr', 'utpr']

        if excludePhotoYr:
            excludeFieldNameList.append('photo_yr')
            
        self.buildAttributeKey(excludeFieldNameList, excludeCC)

        tmpFGDB = srd_temp_ws.SRD_TempWS('tmp_ws')
        
        # Where the exported attributes will be placed.
        tmpTbl = os.path.join(tmpFGDB.FGDBPath, 'tmpAtt')

        tmpFL = 'dissolveByAttributeKey_fl'
        arcpy.MakeFeatureLayer_management(self.fcPath, tmpFL)

        # The unique set of attributes are flagged for extraction.
        sql = '"%s" = 1' % self.attributeKeyFlagFieldName
        arcpy.SelectLayerByAttribute_management(tmpFL, 'NEW_SELECTION', sql)
        selCnt = int(arcpy.GetCount_management(tmpFL).getOutput(0))
        if selCnt < 1:
            raise SRD_Exception('No fields have an attribute key flagged for field: %s' % self.attributeKeyFlagFieldName)

        # Copy all attributes to temporary table.
        self._logChn.logMsg('\n*** Creating unique record attribute table...')
        fcObj = srd_featureclass_tools.SRD_FeatureClassTools()
        excludeFieldList = []
        fcObj.copyFeatureClassSelectedFields(tmpFL, tmpTbl, excludeFieldList, True)

        # Perform the dissolve.
        self._logChn.logMsg('\n*** Performing dissolve...')
        tmpDissolveFC = os.path.join(tmpFGDB.FGDBPath, 'tmpDissolve')
        arcpy.Dissolve_management(self.fcPath, tmpDissolveFC, self.attributeKeyIDFieldName, '', 'SINGLE_PART')

        # Add attributes back on.
        self._logChn.logMsg('\n*** Adding attributes to dissolved feature class...')
        srcFL = 'joinAttribues_fl'
        arcpy.MakeFeatureLayer_management(tmpDissolveFC, srcFL)
        arcpy.AddJoin_management(srcFL, self.attributeKeyIDFieldName, tmpTbl, self.attributeKeyIDFieldName)

        # We pass an empty field name list to the routine so that all
        # fields will be copied to the destination feature class. 
        excludeFieldList = []
        fcObj.copyFeatureClassSelectedFields(srcFL, outFC, excludeFieldList, True)
        
        # Must re-calculate the POLY_NUM to ensure it is unique.
        self._logChn.logMsg('\n*** Renumbering POLY_NUM...')
        oidFieldName = arcpy.Describe(outFC).oidFieldName
        fld = '[%s]' % oidFieldName
        arcpy.CalculateField_management(outFC, 'POLY_NUM', fld)

        # Transfer the attributes that were ignored during dissolve.
        # This will ensure that a feature recieves the same original
        # attribute after the dissolve from one of its parents
        fromToDict = {'initials':'initials', 'uinitials':'uinitials'}
        if ignoreTPR:
            fromToDict['tpr'] = 'tpr'
            fromToDict['utpr'] = 'utpr'

        if excludePhotoYr:
            fromToDict['photo_yr'] = 'photo_yr'
            
        trAttObj = transfer_atts_by_pt.TransferAttsByPt()
        trAttObj.calcAttByPt(self.fcPath, outFC, fromToDict)
        
        # Clean-up
        if arcpy.ListFields(self.fcPath, self.attributeKeyIDFieldName):
            arcpy.DeleteField_management(self.fcPath, self.attributeKeyIDFieldName)

        if arcpy.ListFields(self.fcPath, self.attributeKeyFlagFieldName):
            arcpy.DeleteField_management(self.fcPath, self.attributeKeyFlagFieldName)

        if arcpy.ListFields(outFC, self.attributeKeyIDFieldName):
            arcpy.DeleteField_management(outFC, self.attributeKeyIDFieldName)

        tmpFld = '%s2' % self.attributeKeyIDFieldName
        if arcpy.ListFields(outFC, tmpFld):
            arcpy.DeleteField_management(outFC, tmpFld)

        tmpFld = 'OBJECTID_1'
        if arcpy.ListFields(outFC, tmpFld):
            arcpy.DeleteField_management(outFC, tmpFld)

        if arcpy.ListFields(outFC, self.attributeKeyFlagFieldName):
            arcpy.DeleteField_management(outFC, self.attributeKeyFlagFieldName)

        tmpFGDB.deleteAllTempWS('tmp_ws')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildAttributeDumpDict(self):
        '''
        Builds a dictionary keyed by POLY_NUM that contains the formatted
        overstory/understory record as items in list.

        Returns:
            A dictionary keyed on the POLY_NUM. Each entry in the dictionary
            is a list with the first item being the formatted overstory record
            and the second item the understory.
        '''

        attributeDumpDict = {}

        fieldDisplayLenDict = AVI_RecordUtils.fieldDisplayLength()
        blankAVIRecDict = AVI_RecordUtils.blankAVIRecord()
        
        oFieldNames = self.overstoryFieldNames[:]
        uFieldNames = self.understoryFieldNames[:]

        fieldList = self.allFieldNames
        
        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Building attribute dictionary...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

        with arcpy.da.SearchCursor(self.fcPath, fieldList) as cursor:
            for row in cursor:

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)

                # Build the attribute key using each value of the AVI record
                polyNum = self.fieldValues['poly_num']

                # Make sure key is unique.
                if polyNum in attributeDumpDict:
                    raise SRD_Exception('POLY_NUM: %s duplicated, cannot generate attribute dump' % polyNum)

                # Build overstory/understory record.
                oRec = self.buildFormattedRecString(oFieldNames, fieldDisplayLenDict, blankAVIRecDict)
                
                uRec = self.buildFormattedRecString(uFieldNames, fieldDisplayLenDict, blankAVIRecDict)

                aviRec = [oRec, uRec]

                attributeDumpDict[polyNum] = aviRec

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Attribute dump built\n***')

        return attributeDumpDict
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildAttributeDumpDictByOID(self):
        '''
        Builds a dictionary keyed by OID that contains the formatted
        overstory/understory record as items in list.

        Returns:
            A dictionary keyed on the OID. Each entry in the dictionary
            is a list with the first item being the formatted overstory record
            and the second item the understory.
        '''

        attributeDumpDict = {}

        fieldDisplayLenDict = AVI_RecordUtils.fieldDisplayLength()
        blankAVIRecDict = AVI_RecordUtils.blankAVIRecord()
        
        oFieldNames = self.overstoryFieldNames[:]
        oFieldNames.append('poly_num')
        uFieldNames = self.understoryFieldNames[:]

        # Build list of all fields to read
        fieldList = oFieldNames[:] + uFieldNames[:]
        fieldList.append(self.OIDFieldName)
        
        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Building OID attribute dictionary...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

        with arcpy.da.SearchCursor(self.fcPath, fieldList) as cursor:
            for row in cursor:

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)

                # Build the attribute key using each value of the AVI record
                oid = self.fieldValues[self.OIDFieldName]

                # Make sure key is unique.
                if oid in attributeDumpDict:
                    raise SRD_Exception('OBJECTID: %s duplicated, cannot generate attribute dump' % oid)

                # Build overstory/understory record.
                oRec = self.buildFormattedRecString(oFieldNames, fieldDisplayLenDict, blankAVIRecDict)
                
                uRec = self.buildFormattedRecString(uFieldNames, fieldDisplayLenDict, blankAVIRecDict)

                aviRec = [oRec, uRec]

                attributeDumpDict[oid] = aviRec

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Attribute dump built\n***')

        return attributeDumpDict
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildPolyNumDictByOID(self):
        '''
        Builds a dictionary keyed by OID that contains the formatted
        overstory/understory record as items in list.

        Returns:
            A dictionary keyed on the OID. Each entry in the dictionary
            is a list with the first item being the formatted overstory record
            and the second item the understory.
        '''

        polyNumDict = {}
        
        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Building OID attribute dictionary...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

        with arcpy.da.SearchCursor(self.fcPath, ['OID@', 'poly_num']) as cursor:
            for row in cursor:
                oid = row[0]
                polyNum = row[1]
                if polyNum is None:
                    polyNum = 0

                polyNumDict[oid] = polyNum
                
                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** POLY_NUM dump built\n***')

        return polyNumDict

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildAttributeRecDictByOID(self):
        '''
        Builds a dictionary keyed by OID that contains the formatted
        overstory/understory record as items in list.

        Returns:
            A dictionary keyed on the OID. Each entry in the dictionary
            is a list with the first item being the formatted overstory record
            and the second item the understory.
        '''

        attributeDumpDict = {}

        fieldDisplayLenDict = AVI_RecordUtils.fieldDisplayLength()
        blankAVIRecDict = AVI_RecordUtils.blankAVIRecord()
        
        oFieldNames = self.overstoryFieldNames[:]
        uFieldNames = self.understoryFieldNames[:]
        excludeFieldNames = ('poly_num', 'initials', 'uinitials')
        
        # Build list of all fields to read
        fieldList = oFieldNames[:] + uFieldNames[:]
        fieldList.append(self.OIDFieldName)
        
        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Building OID attribute dictionary...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

        with arcpy.da.SearchCursor(self.fcPath, fieldList) as cursor:
            for row in cursor:

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)

                # Build the attribute key using each value of the AVI record
                oid = self.fieldValues[self.OIDFieldName]

                # Make sure key is unique.
                if oid in attributeDumpDict:
                    raise SRD_Exception('OBJECTID: %s duplicated, cannot generate attribute dump' % oid)

                # Build overstory/understory record.
                oRec = self.buildAttRecString(oFieldNames, fieldDisplayLenDict, blankAVIRecDict, excludeFieldNames)
                
                uRec = self.buildAttRecString(uFieldNames, fieldDisplayLenDict, blankAVIRecDict, excludeFieldNames)

                aviRec = [oRec, uRec]

                attributeDumpDict[oid] = aviRec

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Attribute dump built\n***')

        return attributeDumpDict

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def addOverstoryUnderstoryField(self):
        '''
        '''

        attDict = self.buildAttributeRecDictByOID()

        maxLen = 0
        for oid,attList in attDict.items():
            oAtt = attList[0]
            if len(oAtt) > maxLen:
                maxLen = len(oAtt)
            uAtt = attList[1]
            if len(uAtt) > maxLen:
                maxLen = len(uAtt)

        if maxLen < 1:
            self._logChn.logMsg('Could not determine field length, attributes not added...')
            return False
        
        oFieldName = 'overstory_att'
        if arcpy.ListFields(self.fcPath, oFieldName):
            arcpy.DeleteField_management(self.fcPath, oFieldName)
        arcpy.AddField_management(self.fcPath, oFieldName, 'TEXT', '', '', maxLen)

        uFieldName = 'understory_att'
        if arcpy.ListFields(self.fcPath, uFieldName):
            arcpy.DeleteField_management(self.fcPath, uFieldName)
        arcpy.AddField_management(self.fcPath, uFieldName, 'TEXT', '', '', maxLen)

        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Building Attribute Fields...', 0, selCnt, 1)

        # Maintain a dictionary using the string value of the record as
        # a key with the attribute ID as is value.
        fieldList = ('OID@', oFieldName, uFieldName)
        with arcpy.da.UpdateCursor(self.fcPath, fieldList) as cursor:
            for row in cursor:

                oid = row[0]
                if oid in attDict:
                    attList = attDict[oid]
                    row[1] = attList[0]
                    row[2] = attList[1]
                    cursor.updateRow(row)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Attribute fields built\n***')
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildAttRecString(self, fieldNamesList, fieldDisplayLenDict, blankAVIRecDict, excludeFieldList = []):
        '''
        Takes the field values for the current AVI record and geneates a
        formatted string for the list of field names provided. Each field
        is formatted according to its field length and data type. For string
        fields the field value will be left justified while for a numeric
        field the value will be right justified.

        Parameters:
            fieldNamesList - list of AVI fields to process.

            fieldDisplayLenDict - dictionary indicating length of each field.

            blankAVIRecDict - dictionary indicating default value for a Null
                              field. This is used to determine data type.

            excludeFieldList - list of fields to treat as excluded. These fields
                               are included as part of record but assigned a common
                               value so any comaprison will be same.
        '''

        rec = ''
        for fieldName in fieldNamesList:
            if fieldName in excludeFieldList:
                continue
            fieldDisplayLength = fieldDisplayLenDict[fieldName]
            fieldDefault = blankAVIRecDict[fieldName]
            if self.fieldValues[fieldName]:
                fieldValue = '%s' % self.fieldValues[fieldName]
                rec += fieldValue

        return rec

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildFormattedRecString(self, fieldNamesList, fieldDisplayLenDict, blankAVIRecDict, excludeFieldList = []):
        '''
        Takes the field values for the current AVI record and geneates a
        formatted string for the list of field names provided. Each field
        is formatted according to its field length and data type. For string
        fields the field value will be left justified while for a numeric
        field the value will be right justified.

        Parameters:
            fieldNamesList - list of AVI fields to process.

            fieldDisplayLenDict - dictionary indicating length of each field.

            blankAVIRecDict - dictionary indicating default value for a Null
                              field. This is used to determine data type.

            excludeFieldList - list of fields to treat as excluded. These fields
                               are included as part of record but assigned a common
                               value so any comaprison will be same.
        '''

        rec = ''
        for fieldName in fieldNamesList:
            fieldDisplayLength = fieldDisplayLenDict[fieldName]
            fieldDefault = blankAVIRecDict[fieldName]
            fieldValue = '%s' % self.fieldValues[fieldName]

            # If the field is in the exclusion list then return a string
            # based on length of fields but packed with . character. This
            # is to indicate excluded field to user.
            if fieldName in excludeFieldList:
                fmtStr = ''.ljust(fieldDisplayLength, '.')
            else:
                if fieldDefault == 0:
                    fmtStr = fieldValue.rjust(fieldDisplayLength, ' ')
                else:
                    fmtStr = fieldValue.ljust(fieldDisplayLength, ' ')

            rec += fmtStr

        return rec

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def adjustAttributes(self):
        '''
        Adjust attributes to get rid of some common problems with
        inconsitancies in various datasets.
        
        '''

        # List of fields that will be processed for proper case.

        self._logChn.logMsg('\n***\n*** Adjusting AVI attributes...\n***')

        # Build list of all fields to read
        fieldList = self.overstoryFieldNames[:] + self.understoryFieldNames[:]
        
        # Get mapping of field name to row index.
        fieldIdxDict = {}
        idx = 0
        for fieldName in fieldList:
            fieldIdxDict[fieldName] = idx
            idx += 1
            
        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Adjusting Attributes...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

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

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)

                # Multistoried attribute not needed in understory.
                if self.fieldValues['ustruc'] in ('M', 'S'):
                    self.fieldValues['ustruc'] = ''

                # Sometimes an S is used to indicate a single storied
                # stand, this is not needed
                if self.fieldValues['struc'] == 'S':
                    self.fieldValues['struc'] = ''

                # No modifier extent is required for a CL modifier.
                modFieldList = ('mod1', 'mod2', 'umod1', 'umod2')
                for fieldName in modFieldList:
                    if self.fieldValues[fieldName] == 'CL':
                        modExtField = '%s_ext' % fieldName
                        self.fieldValues[modExtField] = 0
                        
                # If there is a CC or BU it should be in the overstory and not
                # the understory.
                if self.fieldValues['struc'] == 'M':
                    if self.fieldValues['umod1'] == 'CC':
                        if (not self.fieldValues['mod1'] == 'CC') and (not self.fieldValues['mod2'] == 'CC'):
                            if not self.fieldValues['mod1']:
                                self.swapModifiersFromTo('umod1', 'mod1', True)
                            else:
                                if not self.fieldValues['mod2']:
                                    self.swapModifiersFromTo('umod1', 'mod2', True)

                    if self.fieldValues['umod2'] == 'CC':
                        if (not self.fieldValues['mod1'] == 'CC') and (not self.fieldValues['mod2'] == 'CC'):
                            if not self.fieldValues['mod1']:
                                self.swapModifiersFromTo('umod2', 'mod1', True)
                            else:
                                if not self.fieldValues['mod2']:
                                    self.swapModifiersFromTo('umod2', 'mod2', True)

                    if self.fieldValues['umod1'] == 'BU':
                        if (not self.fieldValues['mod1'] == 'BU') and (not self.fieldValues['mod2'] == 'BU'):
                            if not self.fieldValues['mod1']:
                                self.swapModifiersFromTo('umod1', 'mod1', True)
                            else:
                                if not self.fieldValues['mod2']:
                                    self.swapModifiersFromTo('umod1', 'mod2', True)

                    if self.fieldValues['umod2'] == 'BU':
                        if (not self.fieldValues['mod1'] == 'BU') and (not self.fieldValues['mod2'] == 'BU'):
                            if not self.fieldValues['mod1']:
                                self.swapModifiersFromTo('umod2', 'mod1', True)
                            else:
                                if not self.fieldValues['mod2']:
                                    self.swapModifiersFromTo('umod2', 'mod2', True)
                                
                # The first modifier cannot be empty while the second is populated.
                if self.fieldValues['mod2'] and not self.fieldValues['mod1']:
                    self.swapModifiersFromTo('mod2', 'mod1', True)
                if self.fieldValues['umod2'] and not self.fieldValues['umod1']:
                    self.swapModifiersFromTo('umod2', 'umod1', True)

                # Where first modifier is not CC then the most recent Stand Modifier must be second modifier
                if self.fieldValues['mod1']  != 'CC':
                    if self.fieldValues['mod1_yr'] and self.fieldValues['mod2_yr']:
                        if self.fieldValues['mod1_yr'] > self.fieldValues['mod2_yr']:
                            self.swapModifiersFromTo('mod1', 'mod2')

                if self.fieldValues['umod1']  != 'CC':
                    if self.fieldValues['umod1_yr'] and self.fieldValues['umod2_yr']:
                        if self.fieldValues['umod1_yr'] > self.fieldValues['umod2_yr']:
                            self.swapModifiersFromTo('umod1', 'umod2')

                # If one of the modifiers is a CC it should be the first modifier.
                if self.fieldValues['mod2'] == 'CC':
                    if self.fieldValues['mod1'] != 'CC':
                        self.swapModifiersFromTo('mod2', 'mod1')

                if self.fieldValues['umod2'] == 'CC':
                    if self.fieldValues['umod1'] != 'CC':
                        self.swapModifiersFromTo('umod2', 'umod1')

                # Remove understory initials.
                if self.fieldValues['initials'] == '' and self.fieldValues['uinitials'] != '':
                    self.fieldValues['initials'] = self.fieldValues['uinitials']
                self.fieldValues['uinitials'] = ''

                # Update the row before updating.
                for fieldName in fieldList:
                    idx = fieldIdxDict[fieldName]
                    row[idx] = self.fieldValues[fieldName]

                cursor.updateRow(row)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Attribute adjustment completed\n***')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def swapModifiersFromTo(self, fromMod, toMod, clearFromModifier = False):
        '''
        Swaps the contents of one modifier to another modifier.
        This includes extent and year.

        Parameters:
            fromMod - modifer that information will be taken from

            toMod - modifier that will recieve information

            clearFromModifier - If True then the FROM modidfier will be cleared
                                once its information has been transfered to the
                                TO modifier.
        '''

        fromModExt = '%s_ext' % fromMod
        fromModYr = '%s_yr' % fromMod

        toModExt = '%s_ext' % toMod
        toModYr = '%s_yr' % toMod

        # Have the option to clear the FROM modifier. Used to replace
        # overstory modifier with understory then delete understory
        # that was copied.
        if clearFromModifier:
            tmpMod = ''
            tmpModExt = 0
            tmpModYr = 0
        else:
            tmpMod = self.fieldValues[toMod]
            tmpModExt = self.fieldValues[toModExt]
            tmpModYr = self.fieldValues[toModYr]

        # Perform the swap
        self.fieldValues[toMod] = self.fieldValues[fromMod]
        self.fieldValues[toModExt] = self.fieldValues[fromModExt]
        self.fieldValues[toModYr] = self.fieldValues[fromModYr]
        self.fieldValues[fromMod] = tmpMod
        self.fieldValues[fromModExt] = tmpModExt
        self.fieldValues[fromModYr] = tmpModYr
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def swapUnderOverAndDelUnder(self):
        '''
        Was created to move Pine into overstory so that volumes
        could be calculated.
        
        '''

        # List of fields that will be processed for proper case.

        self._logChn.logMsg('\n***\n*** Swapping Over Under...\n***')

        # Build list of all fields to read
        fieldList = self.overstoryFieldNames[:] + self.understoryFieldNames[:]
        
        # Get mapping of field name to row index.
        fieldIdxDict = {}
        idx = 0
        for fieldName in fieldList:
            fieldIdxDict[fieldName] = idx
            idx += 1
            
        selCnt = int(arcpy.GetCount_management(self.fcPath).getOutput(0))
        arcpy.SetProgressor('step', 'Adjusting Attributes...', 0, selCnt, 1)

        rsObj = srd_recordset.SRD_Recordset(self.fcPath)

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

                self.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)

                self.fieldValues['moist_reg'] = self.fieldValues['umoist_reg']
                self.fieldValues['density'] = self.fieldValues['udensity']
                self.fieldValues['height'] = self.fieldValues['uheight']
                self.fieldValues['sp1'] = self.fieldValues['usp1']
                self.fieldValues['sp1_per'] = self.fieldValues['usp1_per']
                self.fieldValues['sp2'] = self.fieldValues['usp2']
                self.fieldValues['sp2_per'] = self.fieldValues['usp2_per']
                self.fieldValues['sp3'] = self.fieldValues['usp3']
                self.fieldValues['sp3_per'] = self.fieldValues['usp3_per']
                self.fieldValues['sp4'] = self.fieldValues['usp4']
                self.fieldValues['sp4_per'] = self.fieldValues['usp4_per']
                self.fieldValues['sp5'] = self.fieldValues['usp5']
                self.fieldValues['sp5_per'] = self.fieldValues['usp5_per']
                self.fieldValues['struc'] = ''
                self.fieldValues['struc_val'] = 0
                self.fieldValues['origin'] = self.fieldValues['uorigin']
                self.fieldValues['tpr'] = self.fieldValues['utpr']
                self.fieldValues['initials'] = self.fieldValues['uinitials']
                self.fieldValues['nfl'] = self.fieldValues['unfl']
                self.fieldValues['nfl_per'] = self.fieldValues['unfl_per']
                self.fieldValues['nat_non'] = self.fieldValues['unat_non']
                self.fieldValues['anth_veg'] = self.fieldValues['uanth_veg']
                self.fieldValues['anth_non'] = self.fieldValues['uanth_non']
                self.fieldValues['mod1'] = self.fieldValues['umod1']
                self.fieldValues['mod1_ext'] = self.fieldValues['umod1_ext']
                self.fieldValues['mod1_yr'] = self.fieldValues['umod1_yr']
                self.fieldValues['mod2'] = self.fieldValues['umod2']
                self.fieldValues['mod2_ext'] = self.fieldValues['umod2_ext']
                self.fieldValues['mod2_yr'] = self.fieldValues['umod2_yr']
                self.fieldValues['data'] = self.fieldValues['udata']
                self.fieldValues['data_yr'] = self.fieldValues['udata_yr']

                self.fieldValues['umoist_reg'] = ''
                self.fieldValues['udensity'] = ''
                self.fieldValues['uheight'] = 0
                self.fieldValues['usp1'] = ''
                self.fieldValues['usp1_per'] = 0
                self.fieldValues['usp2'] = ''
                self.fieldValues['usp2_per'] = 0
                self.fieldValues['usp3'] = ''
                self.fieldValues['usp3_per'] = 0
                self.fieldValues['usp4'] = ''
                self.fieldValues['usp4_per'] = 0
                self.fieldValues['usp5'] = ''
                self.fieldValues['usp5_per'] = 0
                self.fieldValues['ustruc'] = ''
                self.fieldValues['ustruc_val'] = 0
                self.fieldValues['uorigin'] = 0
                self.fieldValues['utpr'] = ''
                self.fieldValues['uinitials'] = ''
                self.fieldValues['unfl'] = ''
                self.fieldValues['unfl_per'] = 0
                self.fieldValues['unat_non'] = ''
                self.fieldValues['uanth_veg'] = ''
                self.fieldValues['uanth_non'] = ''
                self.fieldValues['umod1'] = ''
                self.fieldValues['umod1_ext'] = 0
                self.fieldValues['umod1_yr'] = 0
                self.fieldValues['umod2'] = ''
                self.fieldValues['umod2_ext'] = 0
                self.fieldValues['umod2_yr'] = 0
                self.fieldValues['udata'] = ''
                self.fieldValues['udata_yr'] = 0
                
                # Update the row before updating.
                for fieldName in fieldList:
                    idx = fieldIdxDict[fieldName]
                    row[idx] = self.fieldValues[fieldName]


                cursor.updateRow(row)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

        self._logChn.logMsg('\n***\n*** Attribute adjustment completed\n***')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def calcPrimeCov(self):
        '''
        Calculates the UTM Zone that the polygon is contained in.
        The UTM Zone is determined by projecting the geometry of the
        feature class to Geographic and examining the longitude of the
        polygon. If less than or equal to -114.0 degrees then it is in
        Zone 11 otherwise it is in Zone 12. The centroid of the polygon
        is used to determine the zone.

        Parameters:
            outFC - feature class to recieve the UTM_ZONE.
        '''

        outFC = self.fcPath
        
        fieldDefs = []
        fieldDefs.append(srd_fields.SRD_FieldDef('PRIME_COV','TEXT',10,'Primary Cover'))
        
        fieldDefObj = srd_fields.SRD_FieldDefs(outFC)
        fieldDefObj.setFieldList(fieldDefs)
        fieldDefObj.addFields()

        self._logChn.logMsg('Determining Primary Cover...')

        selCnt = int(arcpy.GetCount_management(outFC).getOutput(0))
        arcpy.SetProgressor("step", "Calculating Primary Cover...", 0, selCnt, 1)

        # Define list of fields required to define primary cover.
        fieldList = ['prime_cov', 'sp1', 'nfl', 'nat_non', 'anth_veg', 'anth_non', 'mod1',
                     'struc', 'struc_val',
                     'usp1', 'unfl', 'unat_non', 'uanth_veg', 'uanth_non', 'umod1']

        # Build a field name based dictionary to access row index for values.
        fieldIdxDict = {}
        idx = 0
        for fieldName in fieldList:
            fieldIdxDict[fieldName] = idx
            idx += 1
            
        with arcpy.da.UpdateCursor(outFC, fieldList) as cursor:
            for row in cursor:

                struc = row[fieldIdxDict['struc']]
                struc_val = row[fieldIdxDict['struc_val']]
                fldPrefix = ''
                # If horizontal a percent of overstory less than 5 then
                # use the understory as the primary cover.
                if struc:
                    if struc == 'H':
                        if struc_val < 5:
                            fldPrefix = 'u'

                # Obtain AVI values needed to determing primary cover.
                sp1 = row[fieldIdxDict['%ssp1' % fldPrefix]]
                nfl = row[fieldIdxDict['%snfl' % fldPrefix]]
                nat_non = row[fieldIdxDict['%snat_non' % fldPrefix]]
                anth_veg = row[fieldIdxDict['%santh_veg' % fldPrefix]]
                anth_non = row[fieldIdxDict['%santh_non' % fldPrefix]]
                mod1 = row[fieldIdxDict['%smod1' % fldPrefix]]

                # Determine the primary cover
                primeCov = ''
                if sp1:
                    primeCov = sp1
                elif anth_veg:
                    primeCov = anth_veg
                elif anth_non:
                    primeCov = anth_non
                elif nfl:
                    primeCov = nfl
                elif nat_non:
                    primeCov = nat_non

                # Account for CPR and shrub.
                if anth_veg and nfl:
                    primeCov = '%s-%s' % (anth_veg, nfl)
                    
                if mod1:
                    primeCov = '%s-%s' % (primeCov, mod1)

                row[fieldIdxDict['prime_cov']] = primeCov
                
                cursor.updateRow(row)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()
    
# ======================================================================
if __name__ == '__main__':

    srcFC = r'D:\test Data\AVI\AVI_REC.gdb\avi_src'
    outFC = r'D:\test Data\AVI\AVI_REC.gdb\avi_out'
    outFC = r'E:\tmp\Tolko_TPR\TPR_Check.gdb\AVI'
    outFC = r'D:\tmp\TDA_TMP\pine_volumes.gdb\UPINE_swapped'
    outFC = r'D:\tmp\TDA_TMP\pine_volumes.gdb\UPINE_swapped'
    outFC = r'D:\pythonscripts\avi_class\test_data\L11_Twp.gdb\L11_AVI'
    
    arcpy.env.overwriteOutput = True
    
    ##arcpy.CopyFeatures_management(srcFC, outFC)
    
    aviObj = AVI_Record(outFC)

    ##aviObj.addOverstoryUnderstoryField()
    
    ##aviObj.flagCoreData()

    ##aviObj.setProperCase()

    ##aviObj.removeBlanks()
    
    ##aviObj.removeNulls()

    ##aviObj.validateDomains()

    ##excludeFieldNameList = ['initials', 'uinitials', 'feat_id']
    ##aviObj.buildAttributeKey(excludeFieldNameList)

    ##aviDict = aviObj.buildAttributeDumpDict()
    
    ##aviDict = aviObj.buildAttributeDumpDictByOID()

    ##aviObj.adjustAttributes()

    dissFC = r'D:\pythonscripts\avi_class\test_data\L11_Twp.gdb\L11_AVI_dissolve'
    aviObj.dissolveByAttributeKey(dissFC, ignoreTPR=True, excludeCC=True, excludePhotoYr=True)
    
    
    ##aviObj.swapUnderOverAndDelUnder()

    
    ##aviObj.calcPrimeCov()
