V1_0.java [src/java/m/wqm/wqmsoilattributes] Revision: 1f50e5e931cecc2cc15a4b155a1c854308cd82c9  Date: Fri Nov 13 18:20:10 MST 2015
package m.wqm.wqmsoilattributes;

/**
 *
 * @author RUMPAL SIDHU
 * @author Shaun Case
 */
import csip.Config;
import csip.ModelDataService;
import csip.ServiceException;
import csip.annotations.Polling;
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 oms3.annotations.Description;
import oms3.annotations.Name;
import org.codehaus.jettison.json.JSONArray;
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 {

    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 String error_msg = "";
    private Connection conn = null;
    private Statement statement = null;
    private wqm.utils.WQMTools.PolygonLatLon polygonData;
    private HashMap<String, V1_0.mukeyData> aoa_mukeyList;
    private boolean useMSSQL;

    @Override
    protected void preProcess() {
	this.error_msg = "";
	this.polygonData = null;

	try {
	    JSONArray request = getRequest().optJSONArray("parameter");

	    if (JSONUtils.checkKeyExistsB(JSONUtils.preprocess(request), "AoAId")) {
		this.aoaId = getStringParam("AoAId");
		this.minimumPercentage = getDoubleParam("aoa_filter_pct", 0.0);
		//this.useMSSQL = getBooleanParam("use_mssql", false);
	        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 = getJSONParam("aoa_geometry").optJSONArray("features");
		if (this.buildPolygon(features)) {
		    this.conn = wqm.utils.WQMTools.getConnection(confString, LOG); //(this.useMSSQL ? "mssql-ssurgo" : "ssurgo" ), LOG);
		    this.statement = this.conn.createStatement();
		}
	    } else {
		//  No valid input stream for this service
		this.error_msg = "Request JSON is missing AoAId. ";
	    }
	} catch (JSONException | ServiceException | SQLException se) {
	    LOG.log(Level.SEVERE, "Could not proceed with preprocessing of request JSON for WQM-02: {0}", se.getMessage());
	    this.error_msg = "Could not proceed with preprocessing of the request JSON. " + se.getMessage();
	}
    }

    @Override
    protected String process() throws Exception {
	if ((null != this.polygonData) && (this.error_msg.isEmpty())) {
	    aoa_mukeyList = this.ssurgoIntersect(this.polygonData.toWKT());
	    try {
		if ((this.error_msg.isEmpty()) && (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 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 = this.statement.executeQuery(query);
		    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);
			if (component.getAoAPct_r() < 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 += " ) ";
			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 < 183 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 < 183 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 = 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("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"));

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

    private Boolean buildPolygon(JSONArray features) throws JSONException {
	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()) {
				    this.error_msg += "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 {
			    this.error_msg = "No coordinates found associated with the polygon specified in feature collection number:  1 . ";
			}
		    } else {
			this.error_msg = "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 {
		this.error_msg = "No geometry found associated with this feature. ";
	    }
	} else {
	    this.error_msg = "Cannot process request JSON, missing features. ";
	}

	return ret_val;
    }

    private Boolean isGeometryPolygonType(JSONObject geometry) {
	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 {
		    this.error_msg = "No valid geometry type found in the feature collection number:  1 .  Looking for 'Polygon'. ";
		}
	    } else {
		this.error_msg = "No geometry type specified in the feature collection number:  1 . ";
	    }
	} else {
	    this.error_msg = "No geometry found in the feature collection number:  1 . ";
	}

	return ret_val;
    }

    private wqm.utils.WQMTools.PolygonLatLon readPolygonCoordinates(JSONArray shape) throws JSONException {
	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 {
	    this.error_msg = "Cannot find the latitude and longitude values for this polygon. ";
	    tPolygon = null;
	}

	return tPolygon;
    }

    // Returns the intersected mukey with the largest area.
    private HashMap<String, V1_0.mukeyData> ssurgoIntersect(String WKTPolygon) {
	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::STPolyFromText(" + WKTPolygon + ",4326).MakeValid().ReorientObject().STIntersection(soilpoly).STArea() / 4046.86 as sizeIntersectionAcres "
			    + "FROM "
			    + "(SELECT m.areasymbol, m.musym, m.mukey, ssurgo.mapunit.muname, geography::STPolyFromText(m.the_geom.STAsText(),4326).MakeValid().ReorientObject() as soilpoly "
			    + "FROM ssurgo.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::STPolyFromText(" + WKTPolygon + ",0)) = 1 "
			    + "AND m.the_geom.STIsValid()=1 "
			    + "AND (geometry::STPolyFromText(" + WKTPolygon + ",0).STIsValid())=1) "
			    + "as a;";
		} 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;";
		}

		ResultSet results = this.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 {
		    this.error_msg += "No results from the intersect query for this geometry. ";
		}
	    }
	} catch (SQLException ex) {
	    this.error_msg += "Cannot continue processing this request in the intersect procedure:  " + ex.getMessage();
	    if (null != ret_val) {
		ret_val.clear();
		ret_val = null;
	    }
	}

	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 = 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.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;
	}

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

	    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;
    }
}