V1_0.java [src/java/m/wqm/wqm02_wqmsoilattributes] Revision:   Date:
package m.wqm.wqm02_wqmsoilattributes;

/**
 *
 * @author RUMPAL SIDHU
 * @author Shaun Case
 */
import csip.Config;
import csip.ModelDataService;
import csip.api.server.ServiceException;
import csip.annotations.Polling;
import csip.annotations.Resource;
import csip.utils.JSONUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;
import javax.ws.rs.Path;
import csip.annotations.Description;
import csip.annotations.Name;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import wqm.utils.DBQueries;
import wqm.utils.DBResources;
import static wqm.utils.DBResources.SSURGO_READONLY;

/**
 *
 * @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)
@Resource(from = DBResources.class)
@Deprecated
public class V1_0 extends ModelDataService {

    static final int JSON_LATITUDE = 1;
    static final int JSON_LONGITUDE = 0;
    private double minimumPercentage = 0.0;

    //Request
    private String aoaId;

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

    private wqm.utils.WQMTools.PolygonLatLon polygonData;
    private HashMap<String, V1_0.mukeyData> aoa_mukeyList;
    private boolean useMSSQL;

    @Override
    protected void preProcess() throws ServiceException {
        try {
            JSONArray request = request().getRequest().optJSONArray("parameter");

            if (JSONUtils.checkKeyExistsB(JSONUtils.preprocess(request), "AoAId")) {
                this.aoaId = parameter().getString("AoAId");
                this.minimumPercentage = parameter().getDouble("aoa_filter_pct", 0.0);
                this.useMSSQL = true;
                LOG.info("Using MS SQL database.");
                /*
                String confString = Config.getString("rse-db", "ssurgo");
                if (confString.contains("rse-sql")) {
                    this.useMSSQL = true;
                    LOG.info("Using MS SQL database.");
                } else {
                    LOG.info("Using PostgreSQL database.");
                }
                 */
                JSONArray features = parameter().getJSON("aoa_geometry").optJSONArray("features");
                if (!this.buildPolygon(features)) {
                    throw new ServiceException("Cannot create an appropriate geometry from aoa_geometry");
                }
            } else {
                //  No valid input stream for this service
                throw new ServiceException("Request JSON is missing AoAId.");
            }
        } catch (JSONException | ServiceException se) {
            LOG.log(Level.SEVERE, "Could not proceed with preprocessing of request JSON for WQM-02: {0}", se);
            throw new ServiceException("Could not proceed with preprocessing of the request JSON. ", se);
        }
    }

    @Override
    protected void doProcess() throws ServiceException {
        if (null != this.polygonData) {
            try (Connection conn = resources().getJDBC(SSURGO_READONLY); Statement statement = conn.createStatement();) {
                aoa_mukeyList = this.ssurgoIntersect(this.polygonData.toWKT(), statement);
                if ((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;
                    String lastQueryWhere = "(";
                    String lastQueryWhere2 = "(";
                    int num_mu = 0;

                    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()) {
                        num_mu++;
                        if (mapCount > 0) {
                            query += ", '" + ite.next().toString() + "' ";
                        } else {
                            query += ite.next().toString() + "' ";
                        }

                        mapCount++;
                    }
                    query += ") AND ssurgo.component.comppct_r IS NOT NULL AND ssurgo.component.hydgrp is not NULL ORDER BY ssurgo.component.mukey, ssurgo.component.cokey, ssurgo.chorizon.hzdept_r DESC, ssurgo.chorizon.kffact;";
                    LOG.log(Level.INFO, "WQM-02-polygon intersect query(118):{0}", query);*/
                    resultSet = statement.executeQuery(DBQueries.WQM02Query01(ite));
                    HashMap<String, Double> aoa_comp_area_list = new HashMap<>();
                    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).area;

                        if (!this.componentMap.containsKey((cokey))) {
                            component = new V1_0.Component(cokey, resultSet.getString("mukey"), 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;

                            double tArea = tComponent.getArea();

                            if (aoa_comp_area_list.containsKey(mukey)) {
                                tArea += aoa_comp_area_list.get(mukey);
                            }
                            aoa_comp_area_list.put(mukey, tArea);

                        } 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) {
                        component.setAoaPct_r(component.getArea() / totalAreas);
                        // 06-08-2016:  Remove any components that have an "empty" taxorder, per request from Christine Whitley at NRCS <Change order>
                        if ((component.getAoAPct_r() < this.minimumPercentage) || (null == component.getTaxorder() ) || component.getTaxorder().isEmpty() || component.getTaxorder().contains(" ")) {
                            LOG.warning("WQM-02:  Deleting component number: " + component.getCokey() + " from result set.  Taxorder is: '" + component.getTaxorder() + "'");
                            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 += " ) ";
                        if (this.useMSSQL) {
                            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), 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 < 165 or nonwet_top_max >= wtbl_bot_max) then 'Perched' else 'Apparent' end as wtkind from WT2";

                        } else {
                            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 < 165 or nonwet_top_max >= wtbl_bot_max) then 'Perched' else 'Apparent' end as wtkind from WT2";
                        }
                        LOG.log(Level.INFO, "WQM-02-polygon intersect query(189):{0}", query);
                        resultSet = 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".
                            if (tcomponent.getWTBL().equalsIgnoreCase("none")) {
                                tcomponent.setWtblTopMin(Double.NaN);
                            } else {
                                tcomponent.setWtblTopMin(resultSet.getDouble("wtbl_top_min"));
                            }
                        }
                    }
                } // Actual Intersect Call failed.
                else {
                    throw new ServiceException("Could not intersect polygons. ");
                }
            } catch (SQLException se) {
                throw new ServiceException("Error executing SQL:  ", se);
            }
        } else {
            throw new ServiceException("Could not create an appropriate polygon from the aoa_geometry");
        }
    }

    @Override
    protected void postProcess() throws Exception {
        results().put("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("mukey", component.getMukey(), "Soil Mapunit Key"));
                tmpArr.put(JSONUtils.dataDesc("muname", this.aoa_mukeyList.get(component.getMukey()).muname, "Soil Mapunit Name"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_area", component.getArea(), "Soil Component Area (Acres) in the Area of Analysis"));
                tmpArr.put(JSONUtils.dataDesc("aoa_pct_r", component.getAoAPct_r(), "Percentage of the Area of Analysis Represented by Soil Component"));
                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"));
                tmpArr.put(JSONUtils.dataDesc("aoa_comp_wtbl_top_min", (Double.isNaN(component.getWtblTopMin()) ? "None" : component.getWtblTopMin()), "Water table top minimum, the minimum value of soimoistdept_r"));

                resultArray.put(JSONUtils.dataDesc("soil_component", tmpArr, "Entry for Soil Component"));
            }
        }
        results().put("soil_component_list", resultArray);
    }

    private Boolean buildPolygon(JSONArray features) throws JSONException, ServiceException {
        Boolean ret_val = false;

        if (null != features) {
            if (features.length() > 0) {
                JSONObject feature = features.getJSONObject(0).optJSONObject("geometry");
                if (this.isGeometryPolygonType(feature)) {
                    JSONArray polygon = feature.optJSONArray("coordinates");
                    if (null != polygon) {
                        if (polygon.length() > 0) {
                            this.polygonData = readPolygonCoordinates(polygon.getJSONArray(0));
                            if (null != this.polygonData) {
                                if (!this.polygonData.isValid()) {
                                    throw new ServiceException("Invalid latitude and/or longitude data contained in this polygon.  Cannot proceed with processing of this request. ");
                                } else {
                                    ret_val = true;
                                }
                            }//No else needed here, the error message will be built in readPolygonCoordinates()
                        } else {
                            throw new ServiceException("No coordinates found associated with the polygon specified in feature collection number:  1 . ");
                        }
                    } else {
                        throw new ServiceException("No coordinates found associated with the polygon specified in feature collection number:  1 . ");
                    }
                }//No else needed here, the error message will be built in isGeometryPolygonType()
            } else {
                throw new ServiceException("No geometry found associated with this feature. ");
            }
        } else {
            throw new ServiceException("Cannot process request JSON, missing features. ");
        }

        return ret_val;
    }

    private Boolean isGeometryPolygonType(JSONObject geometry) throws ServiceException{
        Boolean ret_val = false;

        if (null != geometry) {
            String geometryType;

            geometryType = geometry.optString("type");
            if (!geometryType.isEmpty()) {
                if (geometryType.toLowerCase().equals("polygon")) {
                    ret_val = true;
                } else {
                    throw new ServiceException("No valid geometry type found in the feature collection number:  1 .  Looking for 'Polygon'. ");
                }
            } else {
                throw new ServiceException("No geometry type specified in the feature collection number:  1 . ");
            }
        } else {
            throw new ServiceException("No geometry found in the feature collection number:  1 . ");
        }

        return ret_val;
    }

    private wqm.utils.WQMTools.PolygonLatLon readPolygonCoordinates(JSONArray shape) throws JSONException, ServiceException {
        wqm.utils.WQMTools.PolygonLatLon tPolygon = new wqm.utils.WQMTools.PolygonLatLon();

        if (null != shape) {
            for (int k = 0; k < shape.length(); k++) {
                JSONArray jPoint = shape.getJSONArray(k);
                tPolygon.add(jPoint.getDouble(V1_0.JSON_LATITUDE), jPoint.getDouble(V1_0.JSON_LONGITUDE));
            }
        } else {
            throw new ServiceException("Cannot find the latitude and longitude values for this polygon. ");
        }

        return tPolygon;
    }

    // Returns the intersected mukey with the largest area.
    private HashMap<String, V1_0.mukeyData> ssurgoIntersect(String WKTPolygon, Statement statement) throws ServiceException {
        String polygonText = " ST_PolygonFromText(" + WKTPolygon + ") ";
        String query;
        HashMap<String, V1_0.mukeyData> ret_val = null;

        try {
            if (null != WKTPolygon) {
                ret_val = new HashMap<>();

                if (this.useMSSQL) {
                    /*query = "SELECT areasymbol, musym, mukey, muname, geography::STGeomFromText( intersectPoly.STAsText(), 4326).MakeValid().STArea() / 4046.86 as sizeIntersectionAcres "
                            //+ "geography::STPolyFromText(" + WKTPolygon + ",4326).MakeValid().ReorientObject().STIntersection(soilpoly).STArea() / 4046.86 as    sizeIntersectionAcres "
                            + " FROM "
                            + " (SELECT m.areasymbol, m.musym, m.mukey, ssurgo.mapunit.muname, m.the_geom.STIntersection(geometry::STGeomFromText(" + WKTPolygon + ",0)).MakeValid() as intersectPoly"
                            //+ " geography::STPolyFromText(m.the_geom.STAsText(),4326).MakeValid().ReorientObject() as soilpoly "
                            + " FROM ssurgo.soilmu_a as m "
                            + " WITH (index(geom_sidx)) "
                            + " INNER JOIN ssurgo.mapunit ON m.mukey=ssurgo.mapunit.mukey "
                            + " WHERE m.the_geom.STIntersects(geometry::STGeomFromText(" + WKTPolygon + ",0)) = 1 "
                            + " AND m.the_geom.STIsValid()=1 "
                            + " AND (geometry::STGeomFromText(" + WKTPolygon + ",0).STIsValid())=1) "
                            + " as a;";*/
                    query = DBQueries.WQM02Query02(WKTPolygon);
                } else {
                    /*query = "SELECT m.mukey, ssurgo.mapunit.muname, "
                            + "st_area(st_transform(st_intersection(ST_PolygonFromText(" + WKTPolygon + ", 4326), m.the_geom::geography::geometry ),3541))/43560 as sizeIntersectionAcres "
                            + " FROM ssurgo.soilmu_a as m "
                            + " INNER JOIN ssurgo.mapunit ON m.mukey=ssurgo.mapunit.mukey "
                            + " WHERE ST_Intersects(" + polygonText + ", m.the_geom ) "
                            + " AND st_isvalid( m.the_geom) AND st_isvalid(" + polygonText + ") order by m.mukey;";*/
                    query = DBQueries.WQM02Query03(WKTPolygon, polygonText);
                }

                ResultSet results = statement.executeQuery(query);

                if (null != results) {
                    while (results.next()) {
                        V1_0.mukeyData tMData = new V1_0.mukeyData();
                        String tKey = results.getString("mukey");
                        tMData.area = results.getDouble("sizeIntersectionAcres");
                        tMData.muname = results.getString("muname");

                        if (!ret_val.containsKey(tKey)) {
                            ret_val.put(tKey, tMData);
                        } else {
                            tMData.area += ret_val.get(tKey).area;
                            ret_val.put(tKey, tMData);
                        }
                    }
                } else {
                    throw new ServiceException("No results from the intersect query for this geometry. ");
                }
            }
        } catch (SQLException ex) {
            throw new ServiceException("Cannot continue processing this request in the intersect procedure:  " + ex.getMessage(), ex);
        }

        return ret_val;
    }

    //Inner Classes
    /**
     *
     */
    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;
        private String mukey;
        private double aoa_pct_r;
        Boolean Deleted;

        private ArrayList<V1_0.Component.horizon> chKeys;
        private HashMap<String, V1_0.Component.horizon> horizonMap;

        /**
         *
         * @param cokey
         * @param mukey
         * @param compname
         * @param area
         * @param hsg
         * @param slope_r
         * @param taxorder
         */
        public Component(String cokey, String mukey, 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.mukey = mukey;
            this.name = compname;
            this.area = area;
            this.hsg = hsg;
            this.slope = slope_r;
            this.taxorder = taxorder;
            this.wtbl = "None";
            this.wtbl_top_min = Double.NaN;
            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.aoa_pct_r = 0.0;

            this.slopegr15 = (this.slope > 15.0);
        }

        //Debugging purposes...
        @Override
        public String toString() {
            return this.mukey + "," + this.cokey + "," + this.name + "," + this.area + "," + this.aoa_pct_r + "," + (this.Deleted ? "Deleted" : "Not Deleted");
        }

        //Set Methods
        /**
         *
         * @param deleted
         */
        public void setDeleted(Boolean deleted) {
            this.Deleted = deleted;
        }

        public void setAoaPct_r(double aoa_pct_r) {
            this.aoa_pct_r = aoa_pct_r;
        }

        /**
         *
         * @param wtKind
         */
        public void setWTBL(String wtKind) {
            if (null == wtKind) {
                this.wtbl = "None";
            } else {
                this.wtbl = wtKind;
            }
        }

        /**
         *
         * @param wtbl_top_min
         */
        public void setWtblTopMin(double wtbl_top_min) {
            this.wtbl_top_min = wtbl_top_min;
            this.hwt_lt_24 = (wtbl_top_min <= 61);
        }

        //Get Methods
        public double getAoAPct_r() {
            return this.aoa_pct_r;
        }

        public double getWtblTopMin() {
            return this.wtbl_top_min;
        }

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

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

        public String getMukey() {
            return this.mukey;
        }

        /**
         *
         * @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;
        }

        /**
         *
         * @return
         */
        public String getWTBL() {
            return this.wtbl;
        }

        /**
         *
         * @param chkey
         * @param chfragskey
         * @param kwfact
         * @param kwfact_b
         * @param kffact
         * @param kffact_b
         * @param om_r
         * @param hzthk_r
         * @param hzthk_r_b
         * @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;
            boolean haveHzDepth = 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 (!haveHzDepth) {  //We only want to use the first thickness since these are presorted
                    if (!horizon.getHzthk_r_b()) {
                        this.hzdepth = horizon.getHzdepb_r() - horizon.getHzdept_r();
                    } else {
                        this.hzdepth = horizon.getHzdept_r();
                    }
                    haveHzDepth = true;
                }

                if ((null != this.taxorder) && this.taxorder.equals("Histosols") && !horizon.getKffact_b() && !horizon.getKwfact_b()) {
                    this.kfactor = 0.02;
                } else if (!haveKFactor) {
                    if (horizon.getKffact_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;
            }

            if (profile_thk > 0) {
                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 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;
            }
        }
    }

    protected class mukeyData {

        String muname;
        double area;
    }
}