'''
Name: avi_transfer_att_by_area.py

Purpose: Transfers an attribute from one feature class to another using largest area of intersecting
         polygon

Description:
    
Notes:
    
Author: Doug Crane, Nov, 2012

Modifications:

'''

import sys
import os
import pdb
import shelve
import tempfile

import arcpy

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

import srd.srd_fields as srd_fields
import srd.srd_temp_ws as srd_temp_ws
import srd.srd_featureclass_tools as srd_featureclass_tools

# Add any module variables you wish imported.
__all__ = ['Transfer_By_Area']

__author__ = 'Doug Crane'
__version__ = '1.0'

# ---------------------------------------------------------------
class Transfer_By_Area(object):
    '''
    '''

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

        Parameters:

        Properties:
        '''

        self._logChn = srd_logging.SRD_Log()
        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def addField(self, featureClass, fieldDefObj):
        '''
        Adds required fields for attribute transfer

        Parameters:
            featureClass - feature class to add fields to.
            
            fieldDefObj - field definition for field to add.
        '''
        
        self._logChn.logMsg('Adding Transfer Field')

        fieldDefList = []
        fieldDefList.append(fieldDefObj)
        
        fieldDefObj = srd_fields.SRD_FieldDefs(featureClass)
        fieldDefObj.setFieldList(fieldDefList)
        fieldDefObj.addFields()

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def calcAttByArea(self, fromFeatureClass, toFeatureClass, fromField, toField):
        '''
        This method takes a source feature class and transfers an attribute to
        the output feature class using a largest area method. A unique identifier  is
        first added to the output feature class so its polygons can be linked. An
        intersection is performed using an identity taking the idFieldName from the
        out feature class and all fields from the source feature class. Each intersected
        polygon is processed and a dictionary of field value and area is calculated
        for each idFieldName. This dictionary is then used to update the output feature
        class and assign the attribute with the largest area to the output feature
        based on idFieldName

        Note that the fromField and toField must be similar in structure so that an
        attribue from the fromField can be assigned to the toField.
        
        Parameters:
            fromFeatureClass - the feature class that the attribute will be taken from

            outFeatueClass - the feature class that the attribute will be transfered to.

            fromField - the field name in the fromFeatureClass that will be used.

            toField - the field in the toFeatureClass that will recieve thr fromField value.
        '''

        self._logChn.logMsg('Transfering %s to %s using area method...' % (fromField, toField))

        # Make sure the feature class exists and add the fields
        # that will be calcualted.
        if not arcpy.Exists(toFeatureClass):
            raise SRD_Exception('Feature class: %s not found' % toFeatureClass)

        # Make sure that the source feature class has a known spatial reference.
        sr = arcpy.Describe(toFeatureClass).SpatialReference
        if sr.Name.lower() == 'unknown':
            raise SRD_Exception('No Spatial Reference Found For Input Feature Class %s' % toFeatureClass)

        idFieldName = 'ATT_AREA_ID'
        if not arcpy.ListFields(toFeatureClass, idFieldName):
            arcpy.AddField_management(toFeatureClass, idFieldName, 'LONG')

        if not arcpy.Describe(toFeatureClass).HasOID:
            raise SRD_Exception('No OID Field For Input Feature Class %s' % toFeatureClass)

        # Add unique identifier to use to link FROM attributes to TO attributes.
        oidFieldName = '[' + arcpy.Describe(toFeatureClass).OIDFieldName + ']'
        self._logChn.logMsg('Calculating Feature Identifier ID...')
        arcpy.CalculateField_management(toFeatureClass, idFieldName, oidFieldName)

        # Create a feature layer with only the basic fields.
        # Only the idFieldName will be needed with the intersected dataset
        # since this is what we will use to assign the fromField value
        # to the toField.
        fcObj = srd_featureclass_tools.SRD_FeatureClassTools()
        fieldInfoObj = fcObj.getFieldInfoList(toFeatureClass, [idFieldName,], excludeFields=False)

        toFL = 'to_fl'
        arcpy.MakeFeatureLayer_management(toFeatureClass, toFL, '', '', fieldInfoObj)

        tmpInt = os.path.join(arcpy.env.scratchGDB, 'tmp_int')
        self._logChn.logMsg('Intersecting with source polygons (TMP: %s)...' % tmpInt)
        arcpy.Identity_analysis(toFL, fromFeatureClass, tmpInt, '', 0.001 )
        
        # Build a dictionary for each polygon OID in the resulting set
        # consisting of a dictionary of attribute areas. The shelve module
        # is used due to the potential size of dictionary.
        tmpWFName, tmpWFPath = tempfile.mkstemp('wf','avi_')
        os.close(tmpWFName)
        os.unlink(tmpWFPath)
        polyDict = shelve.open(tmpWFPath)

        self._logChn.logMsg('Accumulating attribute areas...')
        selCnt = int(arcpy.GetCount_management(tmpInt).getOutput(0))
        arcpy.SetProgressor("step", "Accumulating source field attributes...", 0, selCnt, 1)

        # Define fields to be read from feature class.
        fieldList = [fromField, 'SHAPE@AREA', idFieldName]

        with arcpy.da.SearchCursor(tmpInt, fieldList) as cursor:
            for row in cursor:
                attCode = row[0]
                polyArea = row[1]
                polyID = str(row[2])

                # For each polygon maintain a dictionary of attribute codes
                # that the polygon intersects with. As we add attribute codes
                # to this dictionary we check to see if it already has an
                # entry in whcih case we will add the current polygon area
                # to that entry.
                if not attCode is None:
                    if polyID in polyDict:
                        attDict = polyDict[polyID]
                        if attCode in attDict:
                            attArea = attDict[attCode]
                            attArea += polyArea
                            attDict[attCode] = attArea
                        else:
                            attDict[attCode] = polyArea
                        polyDict[polyID] = attDict
                    else:
                        attDict = {}
                        attDict[attCode] = polyArea
                        polyDict[polyID] = attDict

                arcpy.SetProgressorPosition()

        arcpy.ResetProgressor()

        self._logChn.logMsg('Transfering attribute...')
        selCnt = int(arcpy.GetCount_management(toFeatureClass).getOutput(0))
        arcpy.SetProgressor("step", "Assigning attribute...", 0, selCnt, 1)        

        # Define fields to be read from feature class.
        fieldList = [idFieldName, toField, 'OID@']

        with arcpy.da.UpdateCursor(toFeatureClass, fieldList) as cursor:
            for row in cursor:
                polyID = str(row[0])
                oid = row[2]

                if polyID in polyDict:
                    attDict = polyDict[polyID]

                    # Find the entry with the largest area and assign NSR value
                    # to the polygon.
                    maxArea = 0.0
                    maxAtt = ''
                    for key,val in attDict.items():
                        if val > maxArea:
                            maxArea = val
                            maxAtt = key

                    row[1] =  maxAtt
                    cursor.updateRow(row)

                else:
                    self._logChn.logMsg('*** Warning: No ATT dictionary entry for OID %s' % oid)

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()
        arcpy.SetProgressorLabel('Attribute assigment completed...')

        # Clean-up
        if arcpy.ListFields(toFeatureClass, idFieldName):
            arcpy.DeleteField_management(toFeatureClass, idFieldName)

        if arcpy.Exists(toFL):
            arcpy.Delete_management(toFL)
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def transferAttributeByArea(self, fromFeatureClass, toFeatureClass, fromField, toField, toFieldDefObj=None):
        '''
        Transfer attributes from one feature class to another. The attribute
        transfered is taken from the polygon of the fromFeatureClass that covers
        the largest area of the toFeatureClass. The candidate polygons are derived
        from the identity of the fromFeatureClass and toFeatureClass. The fromField
        contained in the fromFeatureClass will be transfered to the toField contained
        in the toFeatureClass.

        The toField can already exist in the toFeatureClass and should be capable of
        holding the values that will be taken from the fromField. If the toField does
        not already exist then there are two options for creating it. You can provide
        a toFieldDefObj that contains the information needed to define it. The other
        option is to use the fromField defintion to create the toField.

        Parameters:
            fromFeatureClass - the feature class containing the attribute you wish transfered.

            toFeatureClass - the feature class that will be assigned the attribute.

            fromField - the field in the fromFeatureClass containing the attribute to transfer.

            toField - the field in the toFeatureClass that will recieve the attribute.

            toFieldDefObj - use this field to define the toFeild that will recieve the attribute.
                            If this object is not passed then it will check the toFeatureClass to
                            determine if the toField already exists and will then use this feild for
                            the attribute. If the field does not exist then it will use the
                            fromField as a template to create the toField.

        '''

        # Check to see if a toField definition has passed in which case it will
        if not toFieldDefObj:
            # If the toField does not exist and no fieldDef is provided then use
            # the fromField definintion to create the toField.
            if not arcpy.ListFields(toFeatureClass, toField):
                for field in arcpy.ListFields(fromFeatureClass):
                    if field.name.lower() == fromField.lower():
                        toFieldDefObj = srd_fields.SRD_FieldDef(toField, field.type, field.length, field.aliasName)
                        self.addField(toFeatureClass, toFieldDefObj)
        else:
            # If the toField exists and a toFieldDefObj is passed then assume the
            # user wishes to remove the old field and add the new one.
            if arcpy.ListFields(toFeatureClass, toField):
                arcpy.DeleteField_management(toFeatureClass, toField)

        # Perform the attribue transfer.
        self.calcAttByArea(fromFeatureClass, toFeatureClass, fromField, toField)
                    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def calcAttByPt(self, fromFeatureClass, toFeatureClass, fieldMapDict, clipByFromFC=False):
        '''
        This method takes a source feature class and transfers an attribute to
        the output feature class using a point method. A unique identifier is
        first added to the TO feature class so its polygons can be linked to the FROM
        polygon points. The FROM feature class is processed to a point feaute class
        and an intersection is performed to the TO feature class. The intersection uses the
        unique identifier that was added to the TO feature class to link each intersected
        feature to its related TO polygon. Attributes are copied to the TO feature class
        based on this identifier.

        Note that the fromField and toField must be similar in structure so that an
        attribue from the fromField can be assigned to the toField.
        
        Parameters:
            fromFeatureClass - the feature class that the attribute will be taken from

            outFeatueClass - the feature class that the attribute will be transfered to.

            fieldMapDict - a dictionary with the key being the FROM field name and the value
                           being the TO field name to recieve attribute values.

        '''

        self._logChn.logMsg('Transfering atributes to using point method...')

        # Make sure the feature class exists and add the fields
        # that will be calcualted.
        if not arcpy.Exists(toFeatureClass):
            raise SRD_Exception('Feature class: %s not found' % toFeatureClass)

        # Make sure that the source feature class has a known spatial reference.
        sr = arcpy.Describe(toFeatureClass).SpatialReference
        if sr.Name.lower() == 'unknown':
            raise SRD_Exception('No Spatial Reference Found For Input Feature Class %s' % toFeatureClass)

        idFieldName = '_ATT_PT_ID'
        if not arcpy.ListFields(toFeatureClass, idFieldName):
            arcpy.AddField_management(toFeatureClass, idFieldName, 'LONG')

        if not arcpy.Describe(toFeatureClass).HasOID:
            raise SRD_Exception('No OID Field For Input Feature Class %s' % toFeatureClass)

        # Add unique identifier to use to link FROM attributes to TO attributes.
        oidFieldName = '[' + arcpy.Describe(toFeatureClass).OIDFieldName + ']'
        self._logChn.logMsg('Calculating Feature Identifier ID...')
        arcpy.CalculateField_management(toFeatureClass, idFieldName, oidFieldName)

        # The TO feature class may have features that extend beyond extents of the
        # FROM feature class (Example: Roads) Clip the TO features using FROM features
        # to ensure that these partial features get a value assigned.
        if clipByFromFC:
            self._logChn.logMsg('Performing clip of TO features using FROM feature class...')
            tmpDissolve =  arcpy.CreateUniqueName('tmp_disolve', arcpy.env.scratchGDB)
            arcpy.Dissolve_management(fromFeatureClass, tmpDissolve, '', '', 'SINGLE_PART')
            tmpFromClip = arcpy.CreateUniqueName('tmp_from_clip', arcpy.env.scratchGDB)
            arcpy.Clip_analysis(toFeatureClass, tmpDissolve, tmpFromClip, .001)
        else:
            tmpFromClip = toFeatureClass
            
        cnt = int(arcpy.GetCount_management(toFeatureClass).getOutput(0))
        self._logChn.logMsg('%s TO features for processing....' % cnt)

        # Create a feature layer with only the basic fields.
        # Only the idFieldName will be needed with the intersected dataset
        # since this is what we will use to assign the fromField value
        # to the toField.
        fcObj = srd_featureclass_tools.SRD_FeatureClassTools()
        fieldInfoObj = fcObj.getFieldInfoList(tmpFromClip, [idFieldName,], excludeFields=False)

        toFL = 'to_fl'
        arcpy.MakeFeatureLayer_management(tmpFromClip, toFL, '', '', fieldInfoObj)
        
        # Each polygon in the TO feature class will be converted to a point
        # so that it may find an intersecting value in the FROM feature class.
        tmpToPt = arcpy.CreateUniqueName('tmp_to_pt', arcpy.env.scratchGDB)
        self._logChn.logMsg('Creating point feature class using TO features...(%s)' % tmpToPt)
        arcpy.FeatureToPoint_management(toFL, tmpToPt, 'INSIDE')

        cnt = int(arcpy.GetCount_management(tmpToPt).getOutput(0))
        self._logChn.logMsg('%s TO features converted to points....' % cnt)

        # Intersection will link TO polygon to a FROM polygon using the TO point
        tmpInt = arcpy.CreateUniqueName('tmp_int', arcpy.env.scratchGDB)
        self._logChn.logMsg('Intersecting with source polygons (%s)...' % tmpInt)
        arcpy.Intersect_analysis([tmpToPt, fromFeatureClass], tmpInt, '', 0.001 )

        self._logChn.logMsg('Accumulating attributes...')
        selCnt = int(arcpy.GetCount_management(tmpInt).getOutput(0))
        if selCnt < 1:
            self._logChn.logWarning('\n***\n*** No points intersected with TO feature class, nothing to transfer\n***')
            return False
        else:
            self._logChn.logMsg('%s TO features intersected with FROM features for attribute update...' % selCnt)
        
        arcpy.SetProgressor("step", "Accumulating source field attributes...", 0, selCnt, 1)

        fromFieldList = fieldMapDict.keys()
        fromFieldList.append(idFieldName)
        fromFldIdx = {value: idx for idx, value in enumerate(fromFieldList)}

        # Build the dictionary of FROM field values that are associated with the
        # Polygon ID field. This will be used to update the TO field values.
        polyIdRecDict = {}
        with arcpy.da.SearchCursor(tmpInt, fromFieldList) as cursor:
            for row in cursor:
                rec = {}
                
                polyID =  row[fromFldIdx[idFieldName]]
                
                # Polygon ID is required to link the FROM and TO values.
                if polyID:
                    for fromFieldName in fromFieldList:
                        rec[fromFieldName] = row[fromFldIdx[fromFieldName]]
                    polyIdRecDict[polyID] = rec
                else:
                    self._logChn.logError('Unable to obtain value for field %s to accumulate FROM values' % idFieldName)
                    
                arcpy.SetProgressorPosition()

        arcpy.ResetProgressor()

        self._logChn.logMsg('Transfering attribute...')
        selCnt = int(arcpy.GetCount_management(toFeatureClass).getOutput(0))
        arcpy.SetProgressor("step", "Assigning attributes...", 0, selCnt, 1)        

        ##pdb.set_trace()
        
        toFieldList = fieldMapDict.values()
        toFieldList.append(idFieldName)
        toFldIdx = {value: idx for idx, value in enumerate(toFieldList)}

        with arcpy.da.UpdateCursor(toFeatureClass, toFieldList) as cursor:
            for row in cursor:
                polyID =  row[toFldIdx[idFieldName]]

                if polyID in polyIdRecDict:
                    fromRecDict = polyIdRecDict[polyID]

                    for fromFld,toFld in fieldMapDict.items():
                        row[toFldIdx[toFld]] = fromRecDict[fromFld]
                        
                    cursor.updateRow(row)

##                else:
##                    self._logChn.logWarning('*** Warning: No ATT %s dictionary entry for  %s' % (idFieldName, polyID))

                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()
        arcpy.SetProgressorLabel('Attribute assigment completed...')

        # Clean-up
        if arcpy.ListFields(toFeatureClass, idFieldName):
            arcpy.DeleteField_management(toFeatureClass, idFieldName)

        if arcpy.Exists(toFL):
            arcpy.Delete_management(toFL)
            
            
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~    
if __name__ == '__main__':

    arcpy.env.overwriteOutput = True
    # If using geographic features then may be best to modify these.
    arcpy.env.XYTolerance = .001
    arcpy.env.XYResolution = .0001
    arcpy.gp.logHistory = False

    toFieldDefObj = srd_fields.SRD_FieldDef('FMU_CODE','TEXT',5,'FMU Code')
    srcFCPathArg = r'D:\testData\AVI\transfer_atts\src_features.gdb\BF_GEO_FMU_POLYGON'
    outFCPathArg = r'V:\AVIE_Warehouse\accpt\avie_warehouse.gdb\AVIE_TOPO\AVIE'
    outFCPathArg = r'D:\testData\AVI\transfer_atts\avie.gdb\avie'
    fromFieldArg = 'FMU_CODE'
    toFieldArg = 'FMU_CODE'
    fromFieldArg = 'FMA_NAME'
    toFieldArg = 'FMA_NAME'

    srcFCPathArg = r'D:\testData\AVI\transfer_atts\avie.gdb\avi1'
    outFCPathArg = r'D:\testData\AVI\transfer_atts\avie.gdb\cut1'
    toFieldArg = 'SP1'
    fromFieldArg = 'SP1'

    myObj = Transfer_By_Area()

    ##myObj.transferAttributeByArea(srcFCPathArg, outFCPathArg, fromFieldArg, toFieldArg)
    
    fromFcArg = r'D:\pythonscripts\avi_class\test_data\tr_att_by_pt.gdb\AVI'
    toFcArg = r'D:\pythonscripts\avi_class\test_data\tr_att_by_pt.gdb\AVIE_prep'
    fromToDict = {'PHOTO_YR':'NEW_PHOTO_YR', 'INITIALS':'NEW_INITIALS'}

    tstFC = r'D:\pythonscripts\avi_class\test_data\tr_att_by_pt.gdb\AVIE_prep_tst'
    if arcpy.Exists(tstFC):
        arcpy.Delete_management(tstFC)
    arcpy.CopyFeatures_management(toFcArg, tstFC)
    toFcArg = tstFC
    
    toFcArg = r'D:\pythonscripts\avi_class\test_data\tr_att_by_pt.gdb\NO_OVERLAP'
    
    fromToDict = {'PHOTO_YR':'PHOTO_YR', 'INITIALS':'INITIALS'}

    fromFcArg = r'E:\old_V\AVIE_Warehouse\data\DMI_P21\dmi_p21_avi_approved.gdb\AVI'
    toFcArg = r'E:\old_V\AVIE_Warehouse\data\DMI_P21\photo_yr.gdb\AVIE_prep'
    fromToDict = {'PHOTO_YR':'PHOTO_YR'}
    clipByFromFCArg = True
    
##    fromFcArg = r'D:\pythonscripts\avi_class\test_data\photo_yr.gdb\P21_photo_yr'
##    toFcArg = r'D:\pythonscripts\avi_class\test_data\photo_yr.gdb\tst1'
##    fromToDict = {'PHOTO_YR':'PHOTO_YR'}
    
    myObj.calcAttByPt(fromFcArg, toFcArg, fromToDict, clipByFromFCArg)
    
