V1_0.java [src/java/m/wqm/wqmsoilattributes] Revision: 96c508e759a26056c1509117ce12d241e0eca2d3  Date: Thu Oct 01 12:03:20 MDT 2015
package m.wqm.wqmsoilattributes;

/**
 *
 * @author RUMPAL SIDHU
 * @author Shaun Case
 */
import oms3.annotations.*;
import csip.ModelDataService;
import csip.annotations.*;
import csip.utils.JSONUtils;
import org.codehaus.jettison.json.JSONArray;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.Path;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

/**
 *
 * @author Shaun Case
 */
@Name("WQM-02: Soil Component Attributes (WQMSoilAttributes)")
@Description("This service intersects area of analysis (AoA) geometry with SSURGO soil mapunit geometry, derives a list of distinct soil components for the AoA, and gets attributes from SSURGO tables required for computing nutrient and pesticide loss potentials.")
@Path("m/wqmsoilattributes/1.0")
@Polling(first = 5000, next = 2000)

public class V1_0 extends ModelDataService {
    
    //mapunit/ssurgo polygon intersect URI
    private final String SSURGO_INTERSECT_URI = "http://csip.engr.colostate.edu:8086/csip-erosion/d/soils/1.2";        
    private final Double minimumPercentage = 0.05;
    
    //Request
    private String aoaId;
    private JSONObject aoaGeometry;

    //Response
    private ArrayList<V1_0.Component> componentList;
    private HashMap<String, V1_0.Component> componentMap;

    private String error_msg = "";
    private Connection conn = null;
    private Statement statement = null; 
    private V1_0.ServiceCall intersectCall = null;
    
    @Override
    protected void preProcess() throws Exception {
        this.error_msg = "";
        
        JSONArray request = getRequest().optJSONArray("parameter");
        
        if ( JSONUtils.checkKeyExistsB( JSONUtils.preprocess(request), "AoAId") ){
            this.aoaId = getStringParam("AoAId");
            //Get the entire aoa_geometry group as it matches the input payload for the ServiceCall class
            this.aoaGeometry = getJSONParam("aoa_geometry");
            this.intersectCall = new ServiceCall( this.SSURGO_INTERSECT_URI );            
            
            try {
                this.conn = wqm.utils.WQMTools.getConnection( "ssurgo", LOG );
                this.statement = this.conn.createStatement();

            } catch (SQLException se) {
                LOG.info("Did not open database for WQM-02");
                LOG.info(se.getMessage());
                this.error_msg = "Could not open the database connection required. ";
            }                                     
        }
        else{
            //  No valid input stream for this service
            this.error_msg = "No valid input parameters were found.  Check your input JSON.";
        }                 
    }

    @Override
    protected String process() throws Exception {       
        if ( ( null != this.intersectCall ) && ( !this.intersectCall.getError() ) ){
            HashMap<String, Double> aoa_mukeyList;               
            aoa_mukeyList = this.intersectCall.intersect( this.aoaGeometry );
            try{
                if ( (!this.intersectCall.getError() ) && ( null != aoa_mukeyList) && ( aoa_mukeyList.size() > 0) ){   
                    ResultSet resultSet;
                    String query;
                    this.componentList = new ArrayList<>();   
                    this.componentMap = new HashMap<>();
                    int mapCount = 0;
                    double totalAreas = 0.0;
                    double aoa_comp_kfact = 0.0;
                    String lastQueryWhere = "(";
                    String lastQueryWhere2 = "(";

                    Set keys = aoa_mukeyList.keySet();                                                
                    Iterator ite = keys.iterator();

                    query = "SELECT ssurgo.component.mukey, ssurgo.component.cokey, ssurgo.component.compname, ssurgo.component.comppct_r, ssurgo.component.hydgrp, "
                            + " ssurgo.component.slope_r, ssurgo.component.taxorder, ssurgo.chorizon.chkey, ssurgo.chorizon.om_r, ssurgo.chorizon.hzthk_r, "
                            + " ssurgo.chorizon.hzdept_r, ssurgo.chorizon.hzdepb_r, ssurgo.chorizon.kwfact, ssurgo.chorizon.kffact, ssurgo.chfrags.fragvol_r, ssurgo.chfrags.chfragskey FROM ssurgo.component "
                            + " LEFT OUTER JOIN ssurgo.chorizon ON ssurgo.chorizon.cokey=ssurgo.component.cokey LEFT OUTER JOIN ssurgo.chfrags on ssurgo.chfrags.chkey=ssurgo.chorizon.chkey WHERE ssurgo.component.mukey in ( '";

                    while (ite.hasNext()) {
                        if ( mapCount > 0 ){
                            query += ", '" + ite.next().toString() + "' ";
                        }
                        else{
                            query += ite.next().toString() + "' ";
                        }   

                        mapCount++;
                    }
                    query += ") AND ssurgo.component.comppct_r IS NOT NULL ORDER BY ssurgo.component.mukey, ssurgo.component.cokey, ssurgo.chorizon.chkey, ssurgo.chorizon.hzdept_r;";
                    resultSet = this.statement.executeQuery( query );
                    while( resultSet.next() ){
                        V1_0.Component tComponent;
                        V1_0.Component component;

                        String mukey = resultSet.getString( "mukey");                 
                        String cokey = resultSet.getString("cokey");  
                        String chkey = resultSet.getString("chkey"); 

                        double aoa_Area = aoa_mukeyList.get( mukey );                   

                        if ( !this.componentMap.containsKey( (cokey) )){
                            component = new V1_0.Component( cokey, resultSet.getString("compname"), (aoa_Area * (resultSet.getDouble("comppct_r") / 100.0)), 
                                                            resultSet.getString("hydgrp"), resultSet.getDouble("slope_r"), resultSet.getString("taxorder") );                           

                            totalAreas += component.getArea();
                            this.componentList.add( component );    
                            this.componentMap.put( cokey, component );
                            tComponent = component;                        
                        }
                        else{                 
                            tComponent = this.componentMap.get( cokey );
                        }

                        //For the rest of these operations, we need to use the temp component pointer..
                        //  Add chkeys to cokey object, some will be duplicates because they are unique by cokey:chkey:chfragkey
                        double kwfact;
                        Boolean kwfact_b;
                        double kffact;
                        Boolean kffact_b;
                        double hzthk_r;
                        Boolean hzthk_r_b;
                        
                        //  Keep these pairs of resultSet calls together..."wasNULL()" depends on the call previous to it.
                        kwfact = resultSet.getDouble("kwfact");
                        kwfact_b = resultSet.wasNull();
                        kffact = resultSet.getDouble("kffact");
                        kffact_b = resultSet.wasNull(); 
                        hzthk_r = resultSet.getDouble("hzthk_r");
                        hzthk_r_b = resultSet.wasNull();                        

                        tComponent.addHorizon(chkey, resultSet.getString("chfragskey"), kwfact, kwfact_b, kffact, kffact_b, resultSet.getDouble("om_r"), hzthk_r, hzthk_r_b, resultSet.getDouble("hzdept_r"), resultSet.getDouble("hzdepb_r"), resultSet.getDouble("fragvol_r"));                    
                    }
                    
                    int componentsRemaining = 0;
                    //Remove components having less than minimumPercentage of total area "totalAreas" here.                
                    for( V1_0.Component component : this.componentList ){
                        if ( (component.getArea() / totalAreas ) < this.minimumPercentage ){
                            component.setDeleted(true);
                        }
                        else{
                            componentsRemaining++;
                            component.computeHorizonResults();   
                            if ( lastQueryWhere.length() > 1 ){
                                lastQueryWhere += " OR component.cokey='" + component.getCokey() + "'";
                                lastQueryWhere2 += " OR WT1.cokey='" + component.getCokey() + "'";
                            }
                            else{
                                lastQueryWhere += " component.cokey='" + component.getCokey() + "'";                            
                                lastQueryWhere2 += "WT1.cokey='" + component.getCokey() + "'";
                            }                        
                        }                                        
                    }                

                    if ( componentsRemaining > 0 ){
                        lastQueryWhere += " ) ";
                        lastQueryWhere2 += " ) ";   
                        query = "With WT1 As (Select component.cokey, component.compname, component.comppct_r, MIN(cosoilmoist.soimoistdept_r) As wtbl_top_min, MAX(cosoilmoist.soimoistdepb_r) As wtbl_bot_max From ssurgo.component Inner Join ssurgo.comonth On component.cokey=comonth.cokey Inner Join ssurgo.cosoilmoist On comonth.comonthkey=cosoilmoist.comonthkey " 
                               +"Where " + lastQueryWhere + "and cosoilmoist.soimoiststat='Wet' Group By component.cokey, component.compname, component.comppct_r Order By component.cokey), WT2 As (Select WT1.cokey, WT1.compname, WT1.comppct_r, WT1.wtbl_top_min, WT1.wtbl_bot_max, MAX(cosoilmoist.soimoistdept_r) As nonwet_top_max From WT1 Left Outer Join ssurgo.comonth On WT1.cokey=comonth.cokey Left Outer Join ssurgo.cosoilmoist On comonth.comonthkey=cosoilmoist.comonthkey " 
                              + "Where " + lastQueryWhere2 + " and (cosoilmoist.soimoiststat NOT IN ('Wet') OR cosoilmoist.soimoiststat IS NULL) Group By WT1.cokey, WT1.compname, WT1.comppct_r, WT1.wtbl_top_min, WT1.wtbl_bot_max) Select WT2.cokey, WT2.compname, WT2.comppct_r, WT2.wtbl_top_min, WT2.wtbl_bot_max, WT2.nonwet_top_max, case when (wtbl_bot_max < 183 or nonwet_top_max >= wtbl_bot_max) then 'Perched' else 'Apparent' end as wtkind from WT2";
                        resultSet = this.statement.executeQuery( query );

                        while ( resultSet.next() ){
                            String tCokey = resultSet.getString("cokey");
                            V1_0.Component tcomponent = this.componentMap.get(tCokey);

                            tcomponent.setWTBL( resultSet.getString("wtkind") );  //  If this is null, the set funciton will make the appropriate adjustment to "None".
                            tcomponent.setWtblTopMin( resultSet.getDouble("wtbl_top_min") );
                        }                                                    
                    }
                } // Actual Intersect Call failed.
                else{
                    this.error_msg += " Could not intersect polygons. ";
                }    
            }
            catch( SQLException se){
                    this.error_msg += "Error executing SQL:  " + se.getMessage();                    
            }            
        }// Creation of Client objec to make call to intersect failed.
        else{
            this.error_msg += " Creation of intersect service call failed. ";            
        }
        
        if ( null != this.conn ){
            if ( null != this.statement ){
                this.statement.close();                
            }
            this.conn.close();                
        }
        return ( !this.error_msg.isEmpty()? this.error_msg : EXEC_OK );
    }

    @Override
    protected void postProcess() throws Exception {
        putResult("AoaId", aoaId, "Area of Analysis Identifier");
        JSONArray resultArray = new JSONArray();
        for (Component component : componentList) {            
            if ( !component.isDeleted() ){
                JSONArray tmpArr = new JSONArray();            
                tmpArr.put(JSONUtils.dataDesc("cokey", component.getCokey(), "Soil Component Key"));
                tmpArr.put(JSONUtils.dataDesc("compname", component.getName(), "Soil Component Name"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_area", component.getArea(), "Soil Component Area (Acres) in the Area of Analysis"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_hsg", component.getHsg(), "Hydrologic Soil Group of the Soil Component"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_taxorder", component.getTaxorder(), "Taxonomic Order of the Soil Component"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_kfact", component.getKfactor(), "K factor of the Surface Mineral Horizon of the Soil Component"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_slope", component.getSlope(), "Slope Percentage of the Soil Component"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_coarse_frag", component.getCoarseFrag(), "Weighted Average Coarse Rock Fragment Volume Percentage through the Profile of the Soil Component"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_om", component.getOrganicMatter(), "Organic Matter Percentage of the  Surface Horizon of the Soil Component"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_hzdepth", component.getHzdepth(), "Depth (inches) of the Surface Horizon of the Soil Component"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_wtbl", component.getWTBL(), "Kind of Water Table of the Soil Component; values are None, Apparent, Perched"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_cracksgr24", component.getCracksgr24(), "Surface Connected Macropores (Cracks) at Least 24 Inches Deep"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_slopegr15", component.getSlopegr15(), "Field Slope is Greater Than 15%"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_hwt_lt_24", component.getHwt_lt_24(), "High Water is Less than 24 Inches Under the Surface"));

                resultArray.put(JSONUtils.dataDesc("soil_component", tmpArr, "Entry for Soil Component"));
            };
        }
        putResult("soil_component_list", resultArray);
    }
        
    
    //Inner Classes

    /**
     *
     */
            

    public class ServiceCall {

        private HashMap<String, Double> aoa_mukeyList;
        private final String URI;
        private final csip.Client newClient;
        private String error_msg;    

        ServiceCall( String URI ){
            this.URI = URI;
            this.error_msg = "";
            this.aoa_mukeyList = new HashMap();
            this.newClient = new csip.Client();   
            if ( this.URI.isEmpty() ){
                this.error_msg = "SSURGO mapunit Intersect URI is empty.";
            }
        }

        /*Calls the csip soil service to intersect AoA and SSURGO layers producing
         set of AoA x mapunit polygons */

        /**
         *
         * @param aoaGeometry
         * @return
         */
        
        public HashMap intersect(JSONObject aoaGeometry) {        
            if ( this.aoa_mukeyList.isEmpty() ){
                try{            
                    JSONObject result;
                    result = this.newClient.doPOST( this.URI, this.createRequest( aoaGeometry ) );                 
                    if  (( null == result ) || ( result.length() <= 0 ) ){
                        this.error_msg += " No data was returned from " + URI + " for this request. "; 
                    }
                    else{
                        //Process result information
                        JSONArray intersectResult = result.optJSONArray("result");
                        
                        if  (intersectResult.length() > 0 ){
                            HashMap<String, Double> tempMap = new HashMap<>();                
                            String mukey;
                            double aoaArea;
                            ArrayList<String> keys = new ArrayList<String>();
                            JSONArray mukeyArray = intersectResult.getJSONArray(1).getJSONArray(0);
                            
                            for (int i = 0; i < mukeyArray.length(); i++) {
                                Map<String, JSONObject> myResult = JSONUtils.preprocess( mukeyArray.getJSONArray(i) );
                                
                                mukey = JSONUtils.getStringParam (myResult,"ssurgo_mukey", "err" );
                                aoaArea = JSONUtils.getDoubleParam( myResult, "acres_in_aoi", 0.0);

                                /*
                                //  Remember to sum the areas of same mukeys...
                                if ( tempMap.containsKey( mukey ) ){
                                    double tArea = tempMap.get( mukey );
                                    aoaArea += tArea;
                                }
                                */
                                
                                tempMap.put(mukey, aoaArea);                                
                                if ( !keys.contains( mukey ) ){
                                    keys.add(mukey);
                                }
                            }  

                            Collections.sort(keys);
                            for (String i : keys) {
                                this.aoa_mukeyList.put(i, tempMap.get(i));
                            }                               
                        }
                        else{
                            //  Check this:  Is this an error or does it just mean that no intersects are found??
                            this.error_msg += " No returned data was found in the result section from the SSURGO mapunit polygon intersect call. ";
                        }                                         
                    }
                }
                catch (Exception ex) {
                    this.error_msg += " Cannot make a connection to that location: " + URI + ".  " + ex.getMessage();
                }               
            }

            return this.aoa_mukeyList;
        }

        /**
         *
         * @return
         */
        public Boolean getError(){return (!this.error_msg.isEmpty());}

        /**
         *
         * @return
         */
        public String getErrorMsg(){return this.error_msg;}



        private JSONObject createRequest( JSONObject aoaGeometry) throws JSONException{
           JSONObject ret_val;
           JSONArray headerArray;
           JSONObject metainfo;

           metainfo = new JSONObject();
           ret_val = new JSONObject();
           headerArray = new JSONArray();       

           metainfo.put( "MultipartRequest", "mapunit intersect request WQM-02");
           metainfo.put( "OriginalSource", getRequestHost() );
           metainfo.put( "OriginalRequest", getRequestURL() );
           metainfo.put( "OriginalSUID", getSUID() );
           ret_val.put("metainfo", metainfo );                     

           headerArray.put(JSONUtils.dataDesc( "AoAI", aoaGeometry, null ) );

           ret_val.put( "parameter", headerArray);           

           return ret_val;          
        }                        
    }

    /**
     *
     */
    public class Component {

        private String cokey; //Soil Component Key
        private String name; //Soil Component Name
        private double area; //Soil Component Area (Acres) in the Area of Analysis
        private String hsg; //Hydrologic Soil Group of the Soil Component
        private String taxorder; //Taxonomic Order of the Soil Component
        private double slope; //Slope Percentage of the Soil Component
        private double kfactor; //K factor of the Surface Mineral Horizon of the Soil Component
        private double coarseFrag; //Weighted Average Coarse Rock Fragment Volume Percentage through the Profile of the Soil Component
        private double organicMatter; //Organic Matter Percentage of the  Surface Horizon of the Soil Component
        private double hzdepth; //Depth (inches) of the Surface Horizon of the Soil Component
        private boolean cracksgr24; //Surface Connected Macropores (Cracks) at Least 24 Inches Deep;  default is False
        private boolean slopegr15; //Field Slope is Greater Than 15%; default is False
        private boolean hwt_lt_24; //High Water is Less than 24 Inches Under the Surface; default is False
        private double wtbl_top_min;
        private String wtbl;
        private double frag_vol_total;
        Boolean Deleted;
        
        
        private ArrayList<V1_0.Component.horizon> chKeys;
        private HashMap<String, V1_0.Component.horizon> horizonMap;

        /**
         *
         * @param cokey
         * @param compname
         * @param area
         * @param hsg
         * @param slope_r
         * @param taxorder
         */
        public Component( String cokey, String compname, double area, String hsg, double slope_r, String taxorder) {
            this.cracksgr24 = false;
            this.slopegr15 = false;
            this.hwt_lt_24 = false;
            this.chKeys = new ArrayList<>();
            this.horizonMap = new HashMap<>();
            
            this.cokey = cokey;
            this.name = compname;
            this.area = area;
            this.hsg = hsg;
            this.slope = slope_r;
            this.taxorder = taxorder;
            this.wtbl = "None";
            this.wtbl_top_min = 0.0;
            this.cracksgr24 = false;  //Set to default false, and is not updated by the current spec.
            this.Deleted = false;
            this.frag_vol_total = 0.0;
            
            this.slopegr15 = ( this.slope > 15.0 );                                   
        }

        //Set Methods

        /**
         *
         * @param key
         */
                public void setCokey(String key) {
            this.cokey = key;
        }

        public void setDeleted( Boolean deleted ){
            this.Deleted = deleted;
        }
        /**
         *
         * @param name
         */
        public void setName(String name) {
            this.name = name;
        }

        /**
         *
         * @param area
         */
        public void setArea(double area) {
            this.area = area;
        }

        /**
         *
         * @param hsg
         */
        public void setHsg(String hsg) {
            this.hsg = hsg;
        }

        /**
         *
         * @param taxorder
         */
        public void setTaxorder(String taxorder) {
            this.taxorder = taxorder;
        }

        /**
         *
         * @param slope
         */
        public void setSlope(double slope) {
            this.slope = slope;
            this.slopegr15 = ( this.slope > 15.0 );                                
        }

        /**
         *
         * @param kfactor
         */
        public void setKfactor(double kfactor) {
            this.kfactor = kfactor;
        }

        /**
         *
         * @param coarseFrag
         */
        public void setCoarseFrag(int coarseFrag) {
            this.coarseFrag = coarseFrag;
        }

        /**
         *
         * @param organicMatter
         */
        public void setOrganicMatter(double organicMatter) {
            this.organicMatter = organicMatter;
        }

        /**
         *
         * @param depth
         */
        public void setHzdepth(double depth) {
            this.hzdepth = depth;
        }

        public void setWTBL( String wtKind ){
            if ( null == wtKind ){
                this.wtbl = "None";
            }
            else{
                this.wtbl = wtKind;
            }                
        }
        
        public void setWtblTopMin( double wtbl_top_min ){
            this.wtbl_top_min = wtbl_top_min;
            this.hwt_lt_24 = (wtbl_top_min <= 61);                
        }
        
        //Get Methods

        /**
         *
         * @return
         */
                public String getCokey() {
            return this.cokey;
        }

        public Boolean isDeleted(){return this.Deleted;}
        
        /**
         *
         * @return
         */
        public String getName() {
            return this.name;
        }

        /**
         *
         * @return
         */
        public double getArea() {
            return this.area;
        }

        /**
         *
         * @return
         */
        public String getHsg() {
            return this.hsg;
        }

        /**
         *
         * @return
         */
        public String getTaxorder() {
            return this.taxorder;
        }

        /**
         *
         * @return
         */
        public double getSlope() {
            return this.slope;
        }

        /**
         *
         * @return
         */
        public double getKfactor() {
            return this.kfactor;
        }

        /**
         *
         * @return
         */
        public double getCoarseFrag() {
            return this.coarseFrag;
        }

        /**
         *
         * @return
         */
        public double getOrganicMatter() {
            return this.organicMatter;
        }

        /**
         *
         * @return
         */
        public double getHzdepth() {
            return this.hzdepth;
        }

        /**
         *
         * @return
         */
        public boolean getCracksgr24() {
            return this.cracksgr24;
        }

        /**
         *
         * @return
         */
        public boolean getSlopegr15() {
            return this.slopegr15;
        }

        /**
         *
         * @return
         */
        public boolean getHwt_lt_24() {
            return this.hwt_lt_24;
        }
        
        public boolean getAoACompHwt(){return this.hwt_lt_24;}        
        public String getWTBL(){return this.wtbl;}
        public double getWtbltopMin(){return this.wtbl_top_min;}
        
        /**
         *
         * @param chkey
         * @param chfragskey
         * @param kwfact
         * @param kwfact_b
         * @param kffact
         * @param kffact_b
         * @param om_r
         * @param hzthk_r
         * @param hzdept_r
         * @param hzdepb_r
         */
        public void addHorizon( String chkey, String chfragskey, double kwfact, boolean kwfact_b, double kffact, boolean kffact_b, double om_r, double hzthk_r, Boolean hzthk_r_b, double hzdept_r, double hzdepb_r, double fragvol_r ){                       
            //  Each component can have mulitiple horizons...each horizon can have multiple fragment volumes...
            V1_0.Component.horizon tHorizon;
            if ( this.horizonMap.containsKey( chkey ) ){
                tHorizon = this.horizonMap.get( chkey );
                tHorizon.addFragKey( chfragskey, fragvol_r );
            }
            else{
                tHorizon = new V1_0.Component.horizon(  chkey,  chfragskey, kwfact,  kwfact_b,  kffact,  kffact_b,  om_r,  hzthk_r, hzthk_r_b,  hzdept_r,  hzdepb_r, fragvol_r );            
                this.chKeys.add( tHorizon );
                this.horizonMap.put( chkey, tHorizon );
            }
        }
        
        public void computeHorizonResults(){
            double profile_thk = 0.0;
            Boolean haveKFactor = false;      
            double comp_product = 0.0;
                        
            //If we remove the "order by" which includes hzdept_r in the first SQL statement in process(), then we need to sort this list before continuing...
            //For now this "order by...hzdept_r" is currently in the SQL statement, so no sort is done here.  If we find that the SQL statement takes longer with
            //The order by clause, and the sort is quicker here, then this code will change.

             //#Get first horizon organic matter            
            this.organicMatter = this.chKeys.get(0).getOm_r();
            
            for( V1_0.Component.horizon horizon : this.chKeys ){
                double horizonThickness = 0.0;
                double horizonProduct = 0.0;
                
                if ( !horizon.getHzthk_r_b() ){
                    this.hzdepth = horizon.getHzdepb_r() - horizon.getHzdept_r();
                }
                else{
                    this.hzdepth = horizon.getHzdept_r();
                }

                if ( this.hsg.equals("D") && this.taxorder.equals("Histosols") && horizon.getKffact_b() && horizon.getKwfact_b() ){
                    this.kfactor = 0.02;
                }
                else{
                    if ( !haveKFactor ){
                        if ( !horizon.getKffact_b() && horizon.getKwfact_b() ){
                            this.kfactor = horizon.getKffact();
                            haveKFactor = true;
                        }
                        else{
                            if( !horizon.getKwfact_b() && horizon.getKffact_b() ){
                                this.kfactor = horizon.getKwfact();
                                haveKFactor = true;
                            }
                        }
                    }
                }
                
                this.frag_vol_total += horizon.getFragVol();
                if ( horizon.getHzthk_r_b() ){
                    horizonThickness = horizon.getHzdepb_r() - horizon.getHzdept_r();
                }
                else{
                    horizonThickness = horizon.getHzthk_r();                    
                }
                profile_thk += horizonThickness;
                horizonProduct = horizonThickness * horizon.getFragVol();
                comp_product += horizonProduct;                                              
            }                        
            
            this.coarseFrag = comp_product / profile_thk;         
        }
        
        class horizon implements Comparable<V1_0.Component.horizon> {
            private final String chkey;
            private final String chfragskey;            
            private final double kwfact;
            private final Boolean kwfact_b;
            private final double kffact;
            private final Boolean kffact_b;
            private final double om_r;
            private final double hzthk_r;
            private final double hzdept_r;
            private final double hzdepb_r;
            private final Boolean hzthk_r_b;
            private double frag_vol_total;
            private HashMap <String, Double> fragkeyMap;
                        
            horizon( String chkey, String chfragskey, double kwfact, boolean kwfact_b, double kffact, boolean kffact_b, double om_r, double hzthk_r, Boolean hzthk_r_b, double hzdept_r, double hzdepb_r, double fragvol_r ){
                this.chkey = chkey;
                this.chfragskey = chfragskey;
                this.kwfact = kwfact;
                this.kwfact_b = kwfact_b;
                this.kffact = kffact;
                this.kffact_b = kffact_b;
                this.om_r = om_r;
                this.hzthk_r = hzthk_r;
                this.hzthk_r_b = hzthk_r_b;
                this.hzdept_r = hzdept_r;
                this.hzdepb_r = hzdepb_r;                                
                this.fragkeyMap = new HashMap<>();
                
                this.fragkeyMap.put(chfragskey, fragvol_r ); 
                this.frag_vol_total = fragvol_r;
            }
            
            @Override
            public int compareTo(V1_0.Component.horizon tHorizon ){
                int ret_val = 0;
                //TODO:  Allow this to be sorted....by chkey:chfragskey:hzdept_r
                //  This might be needed if the "order by" clause of the SQL statement 
                //  used to get this data runs faster without the "order by" that includes this hzdept_r value....
                
                return ret_val;
            }
           
            public void addFragKey( String chfragskey, double fragvol_r ){
                this.fragkeyMap.put( chfragskey, fragvol_r );
                this.frag_vol_total += fragvol_r;
            }
            
            public double getFragVol(){ return this.frag_vol_total; }            
            public String getChkey(){ return this.chkey;}
            public String getChfragsKey(){ return this.chfragskey;}
            public double getKwfact(){ return this.kwfact;}
            public boolean getKwfact_b(){ return this.kwfact_b;}
            public double getKffact(){ return this.kffact;}
            public boolean getKffact_b(){ return this.kffact_b;}
            public double getOm_r(){ return this.om_r;}
            public double getHzthk_r(){ return this.hzthk_r;}
            public Boolean getHzthk_r_b(){ return this.hzthk_r_b;}
            public double getHzdept_r(){ return this.hzdept_r;}
            public double getHzdepb_r(){ return this.hzdepb_r;}                                   
        }
    }
    
}