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

/**
 *
 * @author Shaun Case
 */
import static wqm.utils.DBResources.EROSION_SQLSVR;
import csip.Config;
import csip.ModelDataService;
import csip.api.server.ServiceException;
import csip.SessionLogger;
import csip.annotations.Polling;
import csip.annotations.Resource;
import csip.utils.JSONUtils;
import static gisobjects.GISObject.GISObjectType.polygon;
import gisobjects.GISObjectException;
import static gisobjects.GISObjectFactory.createGISObject;
import static gisobjects.db.GISEngineFactory.createGISEngine;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
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 static soils.AoA.CORRECTED_GEOMETRY;
import static soils.AoA.EXCLUDED_LIST;
import soils.Component;
import soils.Horizon;
import soils.MapUnit;
import soils.SoilsData;
import soils.db.DBResources;
import soils.db.SOILS_DATA;
import soils.db.SOILS_DB_Factory;
import soils.db.tables.TableComponent;
import soils.db.tables.TableComponentCalculations;
import soils.db.tables.TableMapUnit;
import soils.utils.EvalResult;
import static soils.utils.EvalResult.writeDouble;

/**
 * WQM-02: WQMSoilAttributes
 *
 * @author <a href="mailto:shaun.case@colostate.edu">Shaun Case</a>
 *
 */
@Name("WQM-02: Water Quality Soil Parameters (wqmsoilparams)")
@Description("Intersects area of analysis (AoA) geometry with NRCS Soil Data Mart (SDM) mapunit geometry, derives a list of distinct soil components for the AoA, and gets parameters from SDM tables for computing nutrient and pesticide loss potentials")
@Path("m/wqmsoilparams/2.0")

@Resource(from = DBResources.class)
@Resource(from = soils.db.DBResources.class)

public class V2_0 extends ModelDataService {

  //Request
  private String aoaId;
  private JSONObject aoa_geometry;
  private double minimumPercentage = 0.0; // Optional    
  private String newWKT = "";
  private double aoa_area;

  //Response
  private JSONArray results;
  private JSONArray excludeds;

  @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);
        aoa_geometry = parameter().getParamJSON("aoa_geometry");
      } else {
        throw new ServiceException("No AoAId specified.");
      }
    } catch (JSONException se) {
      throw new ServiceException("Could not proceed with preprocessing of the request JSON: " + se.getMessage(), se);
    }
  }

  @Override
  protected void doProcess() throws ServiceException, SQLException, GISObjectException, JSONException, IOException, Exception {
    if (null != aoa_geometry) {
      try (SOILS_DATA soilsDb = SOILS_DB_Factory.createEngine(getClass(), LOG, Config.getString("soils.gis.database.source"));
          Connection gisDb = resources().getJDBC(EROSION_SQLSVR);) {

        V2_0.AoA aoaObject = new AoA(soilsDb, gisDb, aoa_geometry, LOG, minimumPercentage);

        aoaObject.getIntersectionsAndComponents();
        aoa_area = aoaObject.getArea();
        results = aoaObject.toJSON();
        excludeds = aoaObject.getExcluded();
        if (aoaObject.hasGeomChanged()) {
          newWKT = aoaObject.shapeWKT();
        }
      }
    }
  }

  @Override
  protected void postProcess() throws Exception {
    results().put("AoAId", aoaId, "Area of Analysis Identifier");
    results().put("AoA Area", writeDouble(aoa_area, "%.3f"), "acres");
    results().put(SoilsData.MAP_UNIT_LIST, results);
    results().put(EXCLUDED_LIST, excludeds);
    if (!newWKT.isEmpty()) {
      results().put(CORRECTED_GEOMETRY, newWKT, "If this section is present, the input geometry was invalid.  This service attempted to correct it, and was successful in creating a new geometry that could be utilized.  Please check this corrected geometry to be sure it represents what you originally intended.  If it does, please contact the source of your original geometry to have it corrected.  You may use this WKT to do so.");
    }
  }

////////////////////////////////    
//    
//  Inner Classes
//    
//////////////////////////////// 
  /**
   *
   */
  public class AoA extends soils.AoA {

    private double minimumPercentage = 0.0;
    ArrayList<String> removedEmptyTaxorders = new ArrayList<>();
    ArrayList<String> removedAreaToSmall = new ArrayList<>();

    AoA(SOILS_DATA soilsDb, Connection gisDb, JSONObject geometry, SessionLogger LOG, double minimumPercentage) throws GISObjectException, SQLException, JSONException, IOException, ServiceException {
      super(soilsDb, LOG);
      shape = createGISObject(geometry, createGISEngine(gisDb)); //createGISEngine(soilsDb.getConnection()));
      if (shape.getType() != polygon) {//  Also returns this for multipolygons.
        throw new ServiceException("The wqmsoilattributes service only accepts Polygon or MultiPolygon shapes");
      }
      area = shape.areaInAcres();
      this.minimumPercentage = minimumPercentage;
    }

    /**
     *
     * @param component
     */
    public void computeHorizonResults(Component component) {
      double profile_thk = 0.0;
      boolean haveHzDepth = false;
      boolean haveOM_R = false;
      double comp_product = 0.0;
      int counter = 0;

      if (component.horizons.size() > 0) {
        double pH = 0.0;
        double pH_thickness = 0.0;

        for (Horizon horizon : component.horizons.values()) {
          double horizonThickness = 0.0;
          double horizonProduct = 0.0;
          double pH_l = horizon.ph1to1h2o_l();
          double pH_r = horizon.ph1to1h2o_r();
          double pH_h = horizon.ph1to1h2o_h();

//                    if (0 == counter) {
//                        component.calculated_om_r(horizon.om_r());
//                    }
          counter++;

          if (EvalResult.testDefaultDouble(horizon.hzthk_r())) {
            horizonThickness = horizon.hzdepb_r() - horizon.hzdept_r();
          } else {
            horizonThickness = horizon.hzthk_r();
          }

          if ((!haveOM_R) && (!haveHzDepth) && (horizon.selected())) {
            component.calculated_om_r(horizon.om_r());
            component.calculated_hzdepb_r(horizonThickness);
            haveHzDepth = true;
            haveOM_R = true;
          }

          if (EvalResult.testDefaultDouble(pH_r)) {
            if (!EvalResult.testDefaultDouble(pH_h) && !EvalResult.testDefaultDouble(pH_l)) {
              pH_r = (pH_h + pH_l) / 2.0;
              pH_thickness += horizonThickness;
              pH += pH_r * horizonThickness;
            }//Else ignore this layer in the calculation...don't need to do anything else in this case...

          } else {
            pH_thickness += horizonThickness;
            pH += pH_r * horizonThickness;
          }

//                    if (!haveHzDepth) {  //We only want to use the first thickness since these are presorted by the SQL statement in soilsdb
//                        component.calculated_hzdepb_r(horizonThickness);
//                        haveHzDepth = true;
//                    }
          component.calculated_coarse_frag_vol_total(component.calculated_coarse_frag_vol_total() + (EvalResult.testDefaultDouble(horizon.fragvol_r()) ? 0 : horizon.fragvol_r()));

          profile_thk += horizonThickness;
          horizonProduct = horizonThickness * (EvalResult.testDefaultDouble(horizon.fragvol_r()) ? 0 : horizon.fragvol_r());
          comp_product += horizonProduct;
        }

        if (profile_thk > 0.0) {
          component.calculated_coarse_frag(comp_product / profile_thk);
        }
        if (pH_thickness > 0.0) {
          component.calculated_pH(pH / pH_thickness);
        }
      }

    }

    /**
     *
     * @throws ServiceException
     * @throws GISObjectException
     */
    public void getIntersectionsAndComponents() throws ServiceException, GISObjectException {

      if (findIntersectedMapUnitsWithAreas()) {
        if (findAllComponentsApplyWaterFilter()) {
          removeComponentsByAreaSize();
          findComponentSoilMoistures();
          for (MapUnit mapUnit : map_units.values()) {
            if (!mapUnit.isExcluded()) {
              for (Component component : mapUnit.components().values()) {
                if (!component.isExcluded()) {
                  computeHorizonResults(component);
                }
              }
            }
          }
        } else {
          throw new ServiceException("Cannot find components for the map units interesected by this AoA");
        }
      } else {
        throw new ServiceException("Cannot find the mapunits intersected by this AoA");
      }
    }

    private void removeComponentsByAreaSize() {
      for (String mukey : map_units.keySet()) {
        MapUnit tMapUnit = map_units.get(mukey);
        if (!tMapUnit.isExcluded()) {
          for (String cokey : tMapUnit.components().keySet()) {
            Component tComponent = tMapUnit.components().get(cokey);
            if (!tComponent.isExcluded()) {
              if (tComponent.area_pct() < this.minimumPercentage) {
                removedAreaToSmall.add(cokey);
                //tMapUnit.components().remove(cokey);
                tComponent.setExclude(true, "Component removed because its area percentage of the AoI is less than the minimum percentage required by the input parameters.");
              }
            }
          }
        }
      }
    }

    /**
     *
     * @return @throws JSONException
     */
    public JSONArray toJSON() throws JSONException, ServiceException {
      JSONArray mapunitData = new JSONArray();

      for (MapUnit mapUnit : map_units.values()) {
        if (!mapUnit.isExcluded()) {
          JSONArray mapUnitArray = new JSONArray();

          mapUnit.setOutputColumns(new ArrayList<>(Arrays.asList(TableMapUnit.MUKEY, TableMapUnit.MUNAME)));
          mapUnit.mapUnitJSON(mapUnitArray);

          JSONArray componentArray = new JSONArray();
          for (Component component : mapUnit.components().values()) {
            if (!component.isExcluded()) {
              JSONArray componentObject = new JSONArray();

              component.setOutputColumns(new ArrayList<>(Arrays.asList(
                  TableComponent.COKEY, TableComponent.COMPNAME, TableComponent.HYDGRP_NAME,
                  TableComponent.TAXORDER_NAME, TableComponent.SLOPE_R_NAME, TableComponentCalculations.AREA_PCT_NAME,
                  TableComponentCalculations.KFFACT_NAME, TableComponentCalculations.COMP_AREA_NAME, TableComponentCalculations.COARSE_FRAG_NAME,
                  TableComponentCalculations.OM_R_NAME, TableComponentCalculations.HZDEPTH_NAME, TableComponentCalculations.WTBL_NAME,
                  TableComponentCalculations.CRACKS_GR_24_NAME, TableComponentCalculations.SLOPE_GR_15_NAME, TableComponentCalculations.HWT_LT_24_NAME,
                  TableComponentCalculations.WTBL_TOP_MIN_NAME, TableComponentCalculations.PH
              )));
              component.tableColumnsToJSON(componentObject);
              componentArray.put(componentObject);

            }
          }

          if (componentArray.length() > 0) {
            mapUnitArray.put(JSONUtils.data(SoilsData.MAPUNIT_COMPONENT_LIST_NAME, componentArray));
            mapunitData.put(mapUnitArray);
          }

        }
      }

      return mapunitData;
    }
  }
}